from platform import node import ujson as json import re import sys import os import asyncio import threading from module_loader import ModuleLoader, ModuleError, ModuleInstance # Aumentar límite de recursión para soportar programas Tesseract complejos sys.setrecursionlimit(5000) # Justo después de los imports, antes de las clases: _UNRESOLVED = object() # ============================================================================== # Sistema de Errores Tesseract — importar desde tesseract_errors.py # ============================================================================== from tesseract_errors import ( # Base TesseractError, LangError, set_language, get_language, msg, # Léxico / Sintaxis LexError, SyntaxError, IndentError, BlockError, # Nombres y Scope NameError, RedeclarationError, ScopeError, ImportError, # Tipos TypeError, TypeMismatchError, TypeInferenceError, TypeExplicitConflictError, ReturnTypeError, NullTypeError, UnsupportedCastError, # Variables AssignmentError, ConstError, UninitializedError, # Funciones FunctionError, UndefinedFunctionError, ArityError, ArgumentTypeError, ReturnError, RecursionLimitError, # Operaciones OperationError, InvalidOperandError, DivisionByZeroError, OverflowError, BitwiseError, # Condiciones / Flujo ConditionError, BreakOutsideLoopError, ContinueOutsideLoopError, UnreachableCodeError, # Colecciones IndexError, KeyError, IterationError, CollectionLimitError, # Acceso Profundo DeepAccessError, DeepIndexOutOfRangeError, DeepKeyError, DeepNotCollectionError, DeepNullError, DeepPathError, DeepTypeMismatchError, # Rangos RangeError, RangeBoundsError, RangeTypeError, RangeFloatPrecisionError, RangeUnicodeError, RangeUnicodeOrderError, RangeUnicodeCaseMixError, RangeNullMixedError, RangeEmptyError, # OOP ClassError, AttributeError, MethodError, InheritanceError, InstantiationError, FinalExtensionError, InterfaceError, # Structs StructError, StructFieldError, StructFieldConstError, StructFieldTypeError, # Async AsyncError, AwaitOutsideAsyncError, NonAsyncAwaitError, # Runtime RuntimeError, StackOverflowError, IOError, NotImplementedError, InvalidASTError, # Helpers resolve_deep, resolve_unicode, validate_range, format_traceback, make_type_mismatch, make_arity_error, make_undefined_name, make_const_error, make_division_zero, make_deep_out_of_range, make_range_bounds, make_range_type, make_range_float_precision, make_range_unicode, ) try: import tsc as _tsc_engine except ImportError: _tsc_engine = None # Si tsc.py no está disponible, las funciones core # devuelven la config de modo libre por defecto. # ============================================================================== # AGREGADO: excepciones internas del evaluador maestro (reemplazo de eval()) # ============================================================================== # Éstas NO son errores de Tesseract — son señales internas, puramente de # plomería, usadas sólo dentro de la maquinaria de _master_eval / evaluate_ # expression para poder reproducir, en un único lugar (el try/except que # antes envolvía el eval()), EXACTAMENTE los mismos tipos de error que ya # producía el eval() de Python + el except-chain existente (incluyendo el # hecho de que, por el shadowing de NameError/TypeError con las clases de # tesseract_errors, esos casos ya caían siempre al fallback genérico — eso # se replica tal cual, sin "arreglarlo", para no cambiar resultados). class _MEDivZero(Exception): """División/módulo/floor-division entre cero.""" pass class _MEName(Exception): """Identificador que llegó sin resolver hasta el evaluador maestro.""" def __init__(self, name): self.name = name super().__init__(name) class _METypeError(Exception): """Tipos incompatibles en una comparación u operación aritmética.""" def __init__(self, message): self.message = message super().__init__(message) class _MESyntax(Exception): """Texto que no se pudo interpretar como expresión válida.""" def __init__(self, text): self.text = text super().__init__(text) # ============================================================================== # Manejo de Errores — compatibilidad con el código existente # ============================================================================== # InterpreterError apunta a TesseractError para que todos los except existentes # sigan funcionando sin ningún cambio. InterpreterError = TesseractError class UndeclaredVariableError(InterpreterError): def __init__(self, name, file=None, line=None, col=None, function=None): super().__init__( f"Error: La variable '{name}' no ha sido declarada.", file=file, line=line, col=col, function=function ) class InvalidOperationError(InterpreterError): def __init__(self, message, file=None, line=None, col=None, function=None): super().__init__( f"Error de operación inválida: {message}", file=file, line=line, col=col, function=function ) class Symbol: """Contenedor para la información completa de una variable.""" def __init__(self, value, declared_type='dynamic', is_const=False): self.value = value self.declared_type = declared_type self.is_const = is_const # Para const con valor inicial null: permite UNA asignacion posterior. # Una vez asignado un valor no-null, _null_assigned=True y ya no se puede volver a asignar. self._null_assigned = False # True = ya tuvo su primera asignacion post-null def __repr__(self): val = (lambda s: s + '0' if s.endswith('.') else s)(f'{self.value:.15f}'.rstrip('0')) if isinstance(self.value, float) else self.value return f"Symbol(value={val}, type={self.declared_type})" class ThrowSignal(Exception): def __init__(self, exception_class, message=None): self.exception_class = exception_class self.message = message super().__init__(f"[{exception_class}] {message}" if message else f"[{exception_class}]") # ============================================================================== # OOP — Definición de Clase # ============================================================================== class ClassDefinition: def __init__(self, name, parent=None, interfaces=None, modifier=None): self.name = name self.parent = parent # str: nombre clase padre self.interfaces = interfaces or [] # list[str] self.modifier = modifier # 'abstract' | 'final' | None # name -> {value, type, modifier, is_const} self.attributes: dict = {} # name -> {node, params, modifier, explicit_type, is_async} self.methods: dict = {} # {node, params, modifier} | None self.constructor = None def __repr__(self): s = f"ClassDef({self.name}" if self.parent: s += f" extends {self.parent}" if self.interfaces: s += f" implements {', '.join(self.interfaces)}" if self.modifier: s += f" [{self.modifier}]" return s + ")" # ============================================================================== # OOP — Instancia de Clase # ============================================================================== class ClassInstance: _counter = 0 def __init__(self, class_name: str, class_def: ClassDefinition): ClassInstance._counter += 1 self._id = ClassInstance._counter self.class_name = class_name self.class_def = class_def self.attributes: dict = {} # Copiar valores por defecto de los atributos de clase self._init_attributes(class_def) def _init_attributes(self, class_def: ClassDefinition): for attr_name, attr_info in class_def.attributes.items(): self.attributes[attr_name] = attr_info.get('value') def get_attribute(self, name: str): if name in self.attributes: return self.attributes[name] raise AttributeError( msg("class.attribute_missing", attr=name, cls=self.class_name) ) def set_attribute(self, name: str, value): self.attributes[name] = value def __repr__(self): return f"<{self.class_name}#{self._id} attrs={list(self.attributes.keys())}>" # ============================================================================== # OOP — Tabla de Símbolos dedicada a objetos # ============================================================================== class ObjectSymbolTable: def __init__(self): self.class_definitions: dict = {} # str -> ClassDefinition self.interface_definitions: dict = {} # str -> dict # ── Clases ───────────────────────────────────────────────────────────── def declare_class(self, name: str, class_def: ClassDefinition): self.class_definitions[name] = class_def def has_class(self, name: str) -> bool: return name in self.class_definitions def get_class(self, name: str) -> ClassDefinition: if name not in self.class_definitions: raise ClassError( msg("class.undefined", name=name) ) return self.class_definitions[name] # ── Interfaces ────────────────────────────────────────────────────────── def declare_interface(self, name: str, iface_def: dict): self.interface_definitions[name] = iface_def # ── Instanciación ─────────────────────────────────────────────────────── def instantiate(self, class_name: str) -> ClassInstance: return ClassInstance(class_name, self.get_class(class_name)) # ── Polimorfismo: búsqueda de método en jerarquía ─────────────────────── def lookup_method(self, class_name: str, method_name: str): """Busca un método subiendo por la cadena de herencia.""" visited = set() current = class_name while current and current not in visited: visited.add(current) if current in self.class_definitions: cls = self.class_definitions[current] if method_name in cls.methods: return cls.methods[method_name], current current = cls.parent else: break raise MethodError( msg("class.method_missing", method=method_name, cls=class_name) ) # ── Búsqueda de atributo en jerarquía ─────────────────────────────────── def lookup_attribute_def(self, class_name: str, attr_name: str): visited = set() current = class_name while current and current not in visited: visited.add(current) if current in self.class_definitions: cls = self.class_definitions[current] if attr_name in cls.attributes: return cls.attributes[attr_name] current = cls.parent else: break return None # ── isinstance lógico ─────────────────────────────────────────────────── def is_instance_of(self, class_name: str, target: str) -> bool: visited = set() current = class_name while current and current not in visited: visited.add(current) if current == target: return True if current in self.class_definitions: current = self.class_definitions[current].parent else: break return False # ── Debug ──────────────────────────────────────────────────────────────── def __str__(self): if not self.class_definitions and not self.interface_definitions: return " [Sin clases ni interfaces declaradas]" lines = [] for name, cls in self.class_definitions.items(): lines.append(f" Clase '{name}':") if cls.parent: lines.append(f" ↳ Extiende : {cls.parent}") if cls.interfaces: lines.append(f" ↳ Implementa : {', '.join(cls.interfaces)}") if cls.modifier: lines.append(f" ↳ Modificador: {cls.modifier}") attrs = {k: v.get('value') for k, v in cls.attributes.items()} lines.append(f" ↳ Atributos : {attrs}") methods_info = {k: v.get('modifier', 'public') for k, v in cls.methods.items()} lines.append(f" ↳ Métodos : {methods_info}") if cls.constructor: params = list(cls.constructor.get('params', {}).keys()) lines.append(f" ↳ Constructor: ✔ params={params}") for name in self.interface_definitions: lines.append(f" Interfaz '{name}'") return "\n".join(lines) # ============================================================================== # STRUCT SYSTEM — Tipos C-style (StructDefinition, StructInstance, StructSymbolTable) # ============================================================================== class StructFieldDef: """Define un campo dentro de una definición de struct.""" PRIMITIVE_TYPES = frozenset({'int', 'float', 'string', 'bool'}) COLLECTION_TYPES = frozenset({'array', 'tuple', 'dict'}) def __init__(self, name, declared_type='dynamic', is_const=False, initial_value=None, limit=None, is_dynamic=True): self.name = name self.declared_type = declared_type # 'dynamic'|'int'|'float'|'string'|'bool'| # 'array'|'tuple'|'dict'| self.is_const = is_const self.initial_value = initial_value self.limit = limit # int|None — límite de elementos para colecciones self.is_dynamic = is_dynamic def __repr__(self): mod = "const " if self.is_const else "" lim = f"[{self.limit}]" if self.limit is not None else "" return f"StructFieldDef({mod}{self.declared_type}{lim} {self.name} = {self.initial_value!r})" class StructDefinition: """Plano (blueprint) de un tipo struct.""" def __init__(self, name: str): self.name = name self.fields: dict = {} # OrderedDict implícito en Python 3.7+ def add_field(self, fdef: 'StructFieldDef'): self.fields[fdef.name] = fdef def __repr__(self): return f"StructDef({self.name}, fields={list(self.fields.keys())})" class StructInstance: """Instancia concreta de un struct (semántica de valor, como en C).""" _counter = 0 def __init__(self, struct_name: str, struct_def: 'StructDefinition', interpreter: 'object'): StructInstance._counter += 1 self._id = StructInstance._counter self.struct_name = struct_name self.struct_def = struct_def # field_name -> {'value': , 'const_assigned': bool} self.fields: dict = {} self._init_fields(struct_def, interpreter) def _init_fields(self, struct_def: 'StructDefinition', interpreter): import copy for fname, fdef in struct_def.fields.items(): dtype = fdef.declared_type # ¿Es un tipo struct anidado? if (dtype and dtype not in ('dynamic', 'int', 'float', 'string', 'bool', 'array', 'tuple', 'dict', 'null', 'NULL', None) and hasattr(interpreter, 'struct_table') and interpreter.struct_table.has_definition(dtype)): nested_def = interpreter.struct_table.get_definition(dtype) nested_inst = StructInstance(dtype, nested_def, interpreter) self.fields[fname] = {'value': nested_inst, 'const_assigned': False} interpreter._log( f"[STRUCT:INIT] ↳ campo '{fname}' = struct anidado " f"'{dtype}' (instancia #{nested_inst._id})") else: iv = fdef.initial_value if isinstance(iv, (list, dict)): iv = copy.deepcopy(iv) self.fields[fname] = {'value': iv, 'const_assigned': False} mod = "const " if fdef.is_const else "" lim = f"[{fdef.limit}]" if fdef.limit is not None else "" interpreter._log( f"[STRUCT:INIT] ↳ campo '{mod}{fdef.declared_type}{lim} {fname}'" f" = {iv!r}") # ── acceso a campos ────────────────────────────────────────────────────── def get_field(self, name: str): if name not in self.fields: raise StructFieldError( msg("struct.field_missing", field=name, struct=self.struct_name) ) return self.fields[name]['value'] def set_field(self, name: str, value, fdef: 'StructFieldDef' = None): if name not in self.fields: raise StructFieldError( msg("struct.field_missing", field=name, struct=self.struct_name) ) info = self.fields[name] if fdef is None: fdef = self.struct_def.fields.get(name) if fdef and fdef.is_const: if info['const_assigned']: raise StructFieldConstError( msg("struct.field_const_reassign", field=name, struct=self.struct_name) ) info['const_assigned'] = True info['value'] = value # ── formato de impresión (azúcar sintáctico) ───────────────────────────── def format_print(self, path_prefix: str) -> str: """persona — en orden de declaración.""" parts = [] for fname, info in self.fields.items(): val = info['value'] fpath = f"{path_prefix}.{fname}" if isinstance(val, StructInstance): fval = val.format_print(fpath) elif val is None: fval = "null" elif isinstance(val, bool): fval = "true" if val else "false" elif isinstance(val, float): s = f'{val:.15f}'.rstrip('0') fval = s if not s.endswith('.') else s + '0' elif isinstance(val, list): fval = '[' + ', '.join(_fmt_val(i, True) for i in val) + ']' elif isinstance(val, tuple): fval = '(' + ', '.join(_fmt_val(i, True) for i in val) + ')' elif isinstance(val, dict): fval = '{' + ', '.join(f"{k}:{_fmt_val(v, True)}" for k, v in val.items()) + '}' else: fval = str(val) parts.append(f"{fpath}={fval}") return f"{self.struct_name}<{', '.join(parts)}>" def __repr__(self): return f"" class StructSymbolTable: """Tabla de símbolos dedicada a definiciones de struct.""" def __init__(self): self.definitions: dict = {} # name -> StructDefinition def declare(self, name: str, sdef: 'StructDefinition'): self.definitions[name] = sdef def has_definition(self, name: str) -> bool: return name in self.definitions def get_definition(self, name: str, raise_if_missing=True) -> 'StructDefinition': if name not in self.definitions: if not raise_if_missing: return None raise InterpreterError(f"Struct '{name}' no está declarada.", **self._lc()) return self.definitions[name] def __str__(self): if not self.definitions: return " [Sin structs declaradas]" lines = [] for sname, sdef in self.definitions.items(): lines.append(f" Struct '{sname}':") for fname, fdef in sdef.fields.items(): mod = "const " if fdef.is_const else "" lim = f"[{fdef.limit}]" if fdef.limit is not None else "" lines.append( f" ↳ {mod}{fdef.declared_type}{lim} {fname} " f"(default={fdef.initial_value!r})") return "\n".join(lines) # ============================================================================== # Core Type Methods — librería interna oculta # Cada tipo tiene su tabla de métodos con tres categorías: # 'first' — solo después del origen directo # 'chainable' — primera y encadenable # 'chain_only' — solo en cadena (no sobre el origen directo) # 'mutable_default' — muta sin necesidad de .mut (solo colecciones) # 'sig' — AGREGADO: firma legible (nombre + argumentos esperados + # tipo de retorno), usada por el nuevo atributo '.method'. # ============================================================================== def _describe_core_type_methods(type_name: str, methods_table: dict) -> str: """ AGREGADO: arma un listado legible de los métodos/atributos disponibles para un tipo core (int, float, bool, string, array, tuple, dict, null, range), incluyendo la firma esperada (nombre + argumentos) y notas sobre mutabilidad. Es lo que devuelve el atributo '.method' de cada tipo, para consultarlos sin tener que ir a buscar la documentación, ej.: var x = 1; print(x.method); print(1.method); var arr = [1,2,3]; print(arr.method); """ lines = [f"Métodos disponibles para '{type_name}':"] for name in sorted(methods_table.keys()): if name == 'method': continue # no listarse a sí mismo info = methods_table[name] sig = info.get('sig', f"{name}()") notas = [] if info.get('mutable_default'): notas.append('muta la variable directo') elif info.get('cat') == 'chain_only': notas.append('solo encadenado, ej. algo().mut') elif info.get('cat') == 'chainable' and info.get('fn') is not None: notas.append('inmutable — usa .mut para guardar el cambio') nota_str = f" [{', '.join(notas)}]" if notas else "" lines.append(f" - {sig}{nota_str}") return "\n".join(lines) def _core_string_methods(): methods = { # ── first ──────────────────────────────────────────────────────────── 'length': {'cat': 'first', 'fn': lambda v, args: len(str(v))}, 'isEmpty': {'cat': 'first', 'fn': lambda v, args: len(str(v)) == 0}, 'isArray': {'cat': 'first', 'fn': lambda v, args: False}, 'isString': {'cat': 'first', 'fn': lambda v, args: True}, 'isInt': {'cat': 'first', 'fn': lambda v, args: False}, 'isFloat': {'cat': 'first', 'fn': lambda v, args: False}, 'isBool': {'cat': 'first', 'fn': lambda v, args: False}, 'type': {'cat': 'first', 'fn': lambda v, args: 'string'}, # ── chainable ──────────────────────────────────────────────────────── 'toUpperCase': {'cat': 'chainable', 'fn': lambda v, args: str(v).upper()}, 'toLowerCase': {'cat': 'chainable', 'fn': lambda v, args: str(v).lower()}, 'UpperFirst': {'cat': 'chainable', 'fn': lambda v, args: ( str(v)[0].upper() + str(v)[1:] if str(v) and str(v)[0].isalpha() else str(v))}, 'trim': {'cat': 'chainable', 'fn': lambda v, args: str(v).strip()}, 'trimStart': {'cat': 'chainable', 'fn': lambda v, args: str(v).lstrip()}, 'trimEnd': {'cat': 'chainable', 'fn': lambda v, args: str(v).rstrip()}, 'reverse': {'cat': 'chainable', 'fn': lambda v, args: str(v)[::-1]}, 'repeat': {'cat': 'chainable', 'fn': lambda v, args: str(v) * int(args[0]) if args else str(v)}, 'replace': {'cat': 'chainable', 'fn': lambda v, args: str(v).replace(str(args[0]), str(args[1])) if len(args) >= 2 else str(v)}, 'split': {'cat': 'chainable', 'fn': lambda v, args: str(v).split(str(args[0])) if args else list(str(v))}, 'slice': {'cat': 'chainable', 'fn': lambda v, args: str(v)[int(args[0]):int(args[1])] if len(args) >= 2 else str(v)[int(args[0]):]}, 'charAt': {'cat': 'chainable', 'fn': lambda v, args: str(v)[int(args[0])] if args and 0 <= int(args[0]) < len(str(v)) else ''}, 'contains': {'cat': 'chainable', 'fn': lambda v, args: str(args[0]) in str(v) if args else False}, 'startsWith': {'cat': 'chainable', 'fn': lambda v, args: str(v).startswith(str(args[0])) if args else False}, 'endsWith': {'cat': 'chainable', 'fn': lambda v, args: str(v).endswith(str(args[0])) if args else False}, 'indexOf': {'cat': 'chainable', 'fn': lambda v, args: str(v).find(str(args[0])) if args else -1}, 'padStart': {'cat': 'chainable', 'fn': lambda v, args: str(v).rjust(int(args[0]), str(args[1]) if len(args) > 1 else ' ')}, 'padEnd': {'cat': 'chainable', 'fn': lambda v, args: str(v).ljust(int(args[0]), str(args[1]) if len(args) > 1 else ' ')}, # ── conversores (chainable, inmutables por defecto) ─────────────────── 'typeInt': {'cat': 'chainable', 'fn': lambda v, args: int(float(str(v))) if str(v).replace('.','',1).lstrip('-').isdigit() else 0}, 'typeFloat': {'cat': 'chainable', 'fn': lambda v, args: float(str(v)) if str(v).replace('.','',1).lstrip('-').isdigit() else 0.0}, 'typeBool': {'cat': 'chainable', 'fn': lambda v, args: len(str(v)) > 0 and str(v).lower() not in ('false','0','')}, 'typeString': {'cat': 'chainable', 'fn': lambda v, args: str(v)}, 'binary': {'cat': 'chainable', 'fn': lambda v, args: _binary_value(v, args)}, } # AGREGADO: firmas legibles para el nuevo atributo '.method' (no toca # ninguna entrada existente, solo añade metadata 'sig' encima). _sigs = { 'length': 'length() -> int', 'isEmpty': 'isEmpty() -> bool', 'isArray': 'isArray() -> bool', 'isString': 'isString() -> bool', 'isInt': 'isInt() -> bool', 'isFloat': 'isFloat() -> bool', 'isBool': 'isBool() -> bool', 'type': 'type() -> string', 'toUpperCase': 'toUpperCase() -> string', 'toLowerCase': 'toLowerCase() -> string', 'UpperFirst': 'UpperFirst() -> string', 'trim': 'trim() -> string', 'trimStart': 'trimStart() -> string', 'trimEnd': 'trimEnd() -> string', 'reverse': 'reverse() -> string', 'repeat': 'repeat(veces) -> string', 'replace': 'replace(buscado, nuevo) -> string', 'split': 'split(separador) -> array', 'slice': 'slice(inicio, fin) -> string', 'charAt': 'charAt(indice) -> string', 'contains': 'contains(subcadena) -> bool', 'startsWith': 'startsWith(prefijo) -> bool', 'endsWith': 'endsWith(sufijo) -> bool', 'indexOf': 'indexOf(subcadena) -> int', 'padStart': 'padStart(longitud, relleno?) -> string', 'padEnd': 'padEnd(longitud, relleno?) -> string', 'typeInt': 'typeInt() -> int', 'typeFloat': 'typeFloat() -> float', 'typeBool': 'typeBool() -> bool', 'typeString': 'typeString() -> string', 'binary': 'binary(bytes?, signed?) -> string', } for _name, _sig in _sigs.items(): if _name in methods: methods[_name]['sig'] = _sig methods['method'] = {'cat': 'first', 'fn': lambda v, args, _m=methods: _describe_core_type_methods('string', _m)} return methods def _core_int_methods(): methods = { 'length': {'cat': 'first', 'fn': lambda v, args: len(str(int(v)))}, 'isEmpty': {'cat': 'first', 'fn': lambda v, args: False}, 'isArray': {'cat': 'first', 'fn': lambda v, args: False}, 'isString': {'cat': 'first', 'fn': lambda v, args: False}, 'isInt': {'cat': 'first', 'fn': lambda v, args: True}, 'isFloat': {'cat': 'first', 'fn': lambda v, args: False}, 'isBool': {'cat': 'first', 'fn': lambda v, args: False}, 'type': {'cat': 'first', 'fn': lambda v, args: 'int'}, 'abs': {'cat': 'chainable', 'fn': lambda v, args: abs(int(v))}, 'clamp': {'cat': 'chainable', 'fn': lambda v, args: max(int(args[0]), min(int(v), int(args[1]))) if len(args) >= 2 else int(v)}, 'pow': {'cat': 'chainable', 'fn': lambda v, args: int(v) ** int(args[0]) if args else int(v)}, 'max': {'cat': 'chainable', 'fn': lambda v, args: max(int(v), int(args[0])) if args else int(v)}, 'min': {'cat': 'chainable', 'fn': lambda v, args: min(int(v), int(args[0])) if args else int(v)}, 'isEven': {'cat': 'first', 'fn': lambda v, args: int(v) % 2 == 0}, 'isOdd': {'cat': 'first', 'fn': lambda v, args: int(v) % 2 != 0}, 'isPositive': {'cat': 'first', 'fn': lambda v, args: int(v) > 0}, 'isNegative': {'cat': 'first', 'fn': lambda v, args: int(v) < 0}, 'typeFloat': {'cat': 'chainable', 'fn': lambda v, args: float(v)}, 'typeString': {'cat': 'chainable', 'fn': lambda v, args: str(v)}, 'typeBool': {'cat': 'chainable', 'fn': lambda v, args: int(v) != 0}, 'typeInt': {'cat': 'chainable', 'fn': lambda v, args: int(v)}, 'binary': {'cat': 'chainable', 'fn': lambda v, args: _binary_value(v, args)}, # ── AGREGADO ─────────────────────────────────────────────────────── 'toChar': {'cat': 'chainable', 'fn': lambda v, args: chr(int(v)) if 0 <= int(v) <= 0x10FFFF else ''}, 'toHex': {'cat': 'chainable', 'fn': lambda v, args: format(int(v), 'x')}, 'toOctal': {'cat': 'chainable', 'fn': lambda v, args: format(int(v), 'o')}, 'digits': {'cat': 'chainable', 'fn': lambda v, args: [int(d) for d in str(abs(int(v)))]}, } _sigs = { 'length': 'length() -> int', 'isEmpty': 'isEmpty() -> bool', 'isArray': 'isArray() -> bool', 'isString': 'isString() -> bool', 'isInt': 'isInt() -> bool', 'isFloat': 'isFloat() -> bool', 'isBool': 'isBool() -> bool', 'type': 'type() -> string', 'abs': 'abs() -> int', 'clamp': 'clamp(minimo, maximo) -> int', 'pow': 'pow(exponente) -> int', 'max': 'max(otro) -> int', 'min': 'min(otro) -> int', 'isEven': 'isEven() -> bool', 'isOdd': 'isOdd() -> bool', 'isPositive': 'isPositive() -> bool', 'isNegative': 'isNegative() -> bool', 'typeFloat': 'typeFloat() -> float', 'typeString': 'typeString() -> string', 'typeBool': 'typeBool() -> bool', 'typeInt': 'typeInt() -> int', 'binary': 'binary(bytes?, signed?) -> string', 'toChar': 'toChar() -> string', 'toHex': 'toHex() -> string', 'toOctal': 'toOctal() -> string', 'digits': 'digits() -> array', } for _name, _sig in _sigs.items(): if _name in methods: methods[_name]['sig'] = _sig methods['method'] = {'cat': 'first', 'fn': lambda v, args, _m=methods: _describe_core_type_methods('int', _m)} return methods def _core_float_methods(): methods = { 'length': {'cat': 'first', 'fn': lambda v, args: len(str(v))}, 'isEmpty': {'cat': 'first', 'fn': lambda v, args: False}, 'isArray': {'cat': 'first', 'fn': lambda v, args: False}, 'isString': {'cat': 'first', 'fn': lambda v, args: False}, 'isInt': {'cat': 'first', 'fn': lambda v, args: False}, 'isFloat': {'cat': 'first', 'fn': lambda v, args: True}, 'isBool': {'cat': 'first', 'fn': lambda v, args: False}, 'type': {'cat': 'first', 'fn': lambda v, args: 'float'}, 'abs': {'cat': 'chainable', 'fn': lambda v, args: abs(float(v))}, 'round': {'cat': 'chainable', 'fn': lambda v, args: round(float(v), int(args[0])) if args else round(float(v))}, 'floor': {'cat': 'chainable', 'fn': lambda v, args: int(float(v) // 1)}, 'ceil': {'cat': 'chainable', 'fn': lambda v, args: int(-(-float(v) // 1))}, 'clamp': {'cat': 'chainable', 'fn': lambda v, args: max(float(args[0]), min(float(v), float(args[1]))) if len(args) >= 2 else float(v)}, 'pow': {'cat': 'chainable', 'fn': lambda v, args: float(v) ** float(args[0]) if args else float(v)}, 'isNaN': {'cat': 'first', 'fn': lambda v, args: float(v) != float(v)}, 'isInfinite': {'cat': 'first', 'fn': lambda v, args: float(v) in (float('inf'), float('-inf'))}, 'typeInt': {'cat': 'chainable', 'fn': lambda v, args: int(float(v))}, 'typeString': {'cat': 'chainable', 'fn': lambda v, args: str(float(v))}, 'typeBool': {'cat': 'chainable', 'fn': lambda v, args: float(v) != 0.0}, 'typeFloat': {'cat': 'chainable', 'fn': lambda v, args: float(v)}, 'binary': {'cat': 'chainable', 'fn': lambda v, args: _binary_value(v, args)}, } _sigs = { 'length': 'length() -> int', 'isEmpty': 'isEmpty() -> bool', 'isArray': 'isArray() -> bool', 'isString': 'isString() -> bool', 'isInt': 'isInt() -> bool', 'isFloat': 'isFloat() -> bool', 'isBool': 'isBool() -> bool', 'type': 'type() -> string', 'abs': 'abs() -> float', 'round': 'round(decimales?) -> float', 'floor': 'floor() -> int', 'ceil': 'ceil() -> int', 'clamp': 'clamp(minimo, maximo) -> float', 'pow': 'pow(exponente) -> float', 'isNaN': 'isNaN() -> bool', 'isInfinite': 'isInfinite() -> bool', 'typeInt': 'typeInt() -> int', 'typeString': 'typeString() -> string', 'typeBool': 'typeBool() -> bool', 'typeFloat': 'typeFloat() -> float', 'binary': 'binary(bytes?, signed?) -> string', } for _name, _sig in _sigs.items(): if _name in methods: methods[_name]['sig'] = _sig methods['method'] = {'cat': 'first', 'fn': lambda v, args, _m=methods: _describe_core_type_methods('float', _m)} return methods def _core_bool_methods(): methods = { 'type': {'cat': 'first', 'fn': lambda v, args: 'bool'}, 'isArray': {'cat': 'first', 'fn': lambda v, args: False}, 'isString': {'cat': 'first', 'fn': lambda v, args: False}, 'isInt': {'cat': 'first', 'fn': lambda v, args: False}, 'isFloat': {'cat': 'first', 'fn': lambda v, args: False}, 'isBool': {'cat': 'first', 'fn': lambda v, args: True}, 'toggle': {'cat': 'chainable', 'fn': lambda v, args: not bool(v)}, 'typeInt': {'cat': 'chainable', 'fn': lambda v, args: 1 if v else 0}, 'typeString': {'cat': 'chainable', 'fn': lambda v, args: 'true' if v else 'false'}, 'typeFloat': {'cat': 'chainable', 'fn': lambda v, args: 1.0 if v else 0.0}, 'typeBool': {'cat': 'chainable', 'fn': lambda v, args: bool(v)}, } _sigs = { 'type': 'type() -> string', 'isArray': 'isArray() -> bool', 'isString': 'isString() -> bool', 'isInt': 'isInt() -> bool', 'isFloat': 'isFloat() -> bool', 'isBool': 'isBool() -> bool', 'toggle': 'toggle() -> bool', 'typeInt': 'typeInt() -> int', 'typeString': 'typeString() -> string', 'typeFloat': 'typeFloat() -> float', 'typeBool': 'typeBool() -> bool', } for _name, _sig in _sigs.items(): if _name in methods: methods[_name]['sig'] = _sig methods['method'] = {'cat': 'first', 'fn': lambda v, args, _m=methods: _describe_core_type_methods('bool', _m)} return methods _COND_KEYWORDS_RE = re.compile(r'\b(in|and|or|not)\b|==|!=|<=|>=|&&|\|\||<|>') def _looks_like_condition_or_comparison(raw_value: str) -> bool: """ AGREGADO: detecta si un string crudo contiene un operador de comparación o lógico (in, not in, ==, !=, <, >, <=, >=, and, or, not, &&, ||). Si lo tiene, SIEMPRE debe resolverse como expresión real (no guardarse como texto literal). Antes de este fix, 'var valor = 12 in arr;' guardaba LITERALMENTE el texto "12 in arr" como valor de 'valor' (nunca se evaluaba a true/false), porque ninguno de los otros chequeos existentes (operadores aritméticos, punto decimal, identificador simple) reconocía "in" como algo que necesitara resolución. Como un string no vacío es "verdadero" por defecto, cualquier 'if(valor)' posterior daba siempre verdadero sin importar el resultado real de la comparación. """ if not isinstance(raw_value, str): return False return bool(_COND_KEYWORDS_RE.search(raw_value)) def _tesseract_sort_key(x): """ Clave de ordenamiento para el método .sort() de arrays (y estructuras similares), respetando el orden por TIPO que pide la especificación: bool < int/float (numérico) < string < array < tuple < dict Dentro de un mismo tipo, se ordena por el contenido normal (numérico o alfabético). Esto evita el TypeError de Python al comparar tipos incompatibles directamente (ej. '<' not supported between 'str' and 'int'), que es lo que producía sorted(v) plano con arrays mixtos. """ if isinstance(x, bool): return (0, int(x)) if isinstance(x, (int, float)): return (1, x) if isinstance(x, str): return (2, x) if isinstance(x, list): return (3, [_tesseract_sort_key(i) for i in x]) if isinstance(x, tuple): return (4, [_tesseract_sort_key(i) for i in x]) if isinstance(x, dict): return (5, sorted(str(k) for k in x.keys())) # Cualquier otro tipo desconocido: al final, ordenado por su repr textual return (6, str(x)) def _safe_unique(v): """ AGREGADO: versión segura de 'unique' para arrays. La original usaba dict.fromkeys(v), que revienta con TypeError si el array contiene elementos no-hasheables (ej. arrays u otras colecciones anidadas: [[1,2],[1,2],3].unique()). Esta versión compara por igualdad normal (in) en vez de exigir hash, así que funciona igual de bien con escalares y con colecciones anidadas, sin cambiar el resultado para el caso normal (escalares). """ if not isinstance(v, list): return v seen = [] result = [] for item in v: if item not in seen: seen.append(item) result.append(item) return result def _core_array_methods(): import copy methods = { 'length': {'cat': 'first', 'fn': lambda v, args: len(v) if isinstance(v, list) else 0}, 'isEmpty': {'cat': 'first', 'fn': lambda v, args: len(v) == 0 if isinstance(v, list) else True}, 'isArray': {'cat': 'first', 'fn': lambda v, args: True}, 'isString': {'cat': 'first', 'fn': lambda v, args: False}, 'isInt': {'cat': 'first', 'fn': lambda v, args: False}, 'isFloat': {'cat': 'first', 'fn': lambda v, args: False}, 'isBool': {'cat': 'first', 'fn': lambda v, args: False}, 'type': {'cat': 'first', 'fn': lambda v, args: 'array'}, 'first': {'cat': 'first', 'fn': lambda v, args: v[0] if isinstance(v, list) and v else None}, 'last': {'cat': 'first', 'fn': lambda v, args: v[-1] if isinstance(v, list) and v else None}, # ── mutables por defecto ────────────────────────────────────────────── 'push': {'cat': 'chainable', 'mutable_default': True, 'fn': lambda v, args: v + [args[0]] if isinstance(v, list) and args else v}, 'pop': {'cat': 'chainable', 'mutable_default': True, 'fn': lambda v, args: v[:-1] if isinstance(v, list) and v else v}, 'shift': {'cat': 'chainable', 'mutable_default': True, 'fn': lambda v, args: v[1:] if isinstance(v, list) and v else v}, 'unshift': {'cat': 'chainable', 'mutable_default': True, 'fn': lambda v, args: [args[0]] + v if isinstance(v, list) and args else v}, 'insert': {'cat': 'chainable', 'mutable_default': True, 'fn': lambda v, args: v[:int(args[0])] + [args[1]] + v[int(args[0]):] if isinstance(v, list) and len(args) >= 2 else v}, 'remove': {'cat': 'chainable', 'mutable_default': True, 'fn': lambda v, args: [x for x in v if x != args[0]] if isinstance(v, list) and args else v}, 'clear': {'cat': 'chainable', 'mutable_default': True, 'fn': lambda v, args: []}, # ── inmutables (necesitan .mut para mutar) ──────────────────────────── # REPARADO: 'sort' usaba sorted(v) plano, que revienta con TypeError # apenas el array mezcla tipos (ej. str con int). Ahora usa # _tesseract_sort_key, que ordena por tipo primero (bool, int/float, # string, array, tuple, dict) y por contenido dentro de cada tipo. 'sort': {'cat': 'chainable', 'fn': lambda v, args: sorted(v, key=_tesseract_sort_key) if isinstance(v, list) else v}, 'reverse': {'cat': 'chainable', 'fn': lambda v, args: list(reversed(v)) if isinstance(v, list) else v}, 'filter': {'cat': 'chainable', 'fn': lambda v, args: v}, # args lo evalúa el handler 'map': {'cat': 'chainable', 'fn': lambda v, args: v}, 'slice': {'cat': 'chainable', 'fn': lambda v, args: v[int(args[0]):int(args[1])] if isinstance(v, list) and len(args) >= 2 else v[int(args[0]):] if args else v}, 'concat': {'cat': 'chainable', 'fn': lambda v, args: v + (args[0] if isinstance(args[0], list) else [args[0]]) if isinstance(v, list) and args else v}, 'contains': {'cat': 'chainable', 'fn': lambda v, args: args[0] in v if isinstance(v, list) and args else False}, 'indexOf': {'cat': 'chainable', 'fn': lambda v, args: v.index(args[0]) if isinstance(v, list) and args and args[0] in v else -1}, 'join': {'cat': 'chainable', 'fn': lambda v, args: str(args[0]).join(str(x) for x in v) if isinstance(v, list) else str(v)}, # REPARADO: dict.fromkeys(v) revienta con arrays/colecciones anidadas # no-hasheables dentro del array; ver _safe_unique. 'unique': {'cat': 'chainable', 'fn': lambda v, args: _safe_unique(v)}, 'flatten': {'cat': 'chainable', 'fn': lambda v, args: (lambda _flat: _flat)([ x for sub in v for x in ( list(sub.values()) if isinstance(sub, dict) else list(sub) if isinstance(sub, tuple) else sub if isinstance(sub, list) else [sub] ) ]) if isinstance(v, list) else v}, 'typeString': {'cat': 'chainable', 'fn': lambda v, args: str(v)}, # ── AGREGADO: .kinds — array con el tipo de cada elemento, en el # mismo orden (shallow: un sub-array/tupla/dict se reporta como # 'array'/'tuple'/'dict' sin bajar a sus propios elementos; para # ver los tipos de un sub-array hay que indexar hasta él y # llamar .kinds sobre ese índice). Inmutable por defecto — como # cualquier otro core method, se le puede encadenar .mut. 'kinds': {'cat': 'chainable', 'fn': lambda v, args: [_get_value_type(x) for x in v] if isinstance(v, list) else v}, # ── solo encadenables (chain_only) ──────────────────────────────────── 'mut': {'cat': 'chain_only','fn': None}, # manejado por el dispatcher } _sigs = { 'length': 'length() -> int', 'isEmpty': 'isEmpty() -> bool', 'isArray': 'isArray() -> bool', 'isString': 'isString() -> bool', 'isInt': 'isInt() -> bool', 'isFloat': 'isFloat() -> bool', 'isBool': 'isBool() -> bool', 'type': 'type() -> string', 'first': 'first() -> any', 'last': 'last() -> any', 'push': 'push(valor) -> array', 'pop': 'pop() -> array', 'shift': 'shift() -> array', 'unshift': 'unshift(valor) -> array', 'insert': 'insert(indice, valor) -> array', 'remove': 'remove(valor) -> array', 'clear': 'clear() -> array', 'sort': 'sort() -> array', 'reverse': 'reverse() -> array', 'filter': 'filter(condicion) -> array', 'map': 'map(funcion) -> array', 'slice': 'slice(inicio, fin?) -> array', 'concat': 'concat(otroArray) -> array', 'contains': 'contains(valor) -> bool', 'indexOf': 'indexOf(valor) -> int', 'join': 'join(separador) -> string', 'unique': 'unique() -> array', 'flatten': 'flatten() -> array', 'typeString': 'typeString() -> string', 'kinds': 'kinds() -> array — tipo de cada elemento (shallow), ej. [21,3.14,"x"].kinds() -> [int,float,string]', 'mut': "mut [terminador de cadena, ej. arr.sort().mut]", } for _name, _sig in _sigs.items(): if _name in methods: methods[_name]['sig'] = _sig methods['method'] = {'cat': 'first', 'fn': lambda v, args, _m=methods: _describe_core_type_methods('array', _m)} return methods def _core_null_methods(): methods = { 'isNull': {'cat': 'first', 'fn': lambda v, args: True}, 'isArray': {'cat': 'first', 'fn': lambda v, args: False}, 'isString': {'cat': 'first', 'fn': lambda v, args: False}, 'isInt': {'cat': 'first', 'fn': lambda v, args: False}, 'isFloat': {'cat': 'first', 'fn': lambda v, args: False}, 'isBool': {'cat': 'first', 'fn': lambda v, args: False}, 'type': {'cat': 'first', 'fn': lambda v, args: 'null'}, # ── Conversores: null → valor vacío del tipo destino ───────────────── # Todos son inmutables por defecto (.mut los muta igual que otros tipos) 'typeInt': {'cat': 'chainable', 'fn': lambda v, args: 0}, 'typeFloat': {'cat': 'chainable', 'fn': lambda v, args: 0.0}, 'typeBool': {'cat': 'chainable', 'fn': lambda v, args: False}, 'typeString': {'cat': 'chainable', 'fn': lambda v, args: ''}, 'typeArray': {'cat': 'chainable', 'fn': lambda v, args: []}, 'typeDict': {'cat': 'chainable', 'fn': lambda v, args: {}}, 'typeTuple': {'cat': 'chainable', 'fn': lambda v, args: ()}, 'typeRange': {'cat': 'chainable', 'fn': lambda v, args: list(range(0, 0))}, # ── AGREGADO ─────────────────────────────────────────────────────── 'orElse': {'cat': 'chainable', 'fn': lambda v, args: args[0] if args else None}, 'isPresent': {'cat': 'first', 'fn': lambda v, args: False}, } _sigs = { 'isNull': 'isNull() -> bool', 'isArray': 'isArray() -> bool', 'isString': 'isString() -> bool', 'isInt': 'isInt() -> bool', 'isFloat': 'isFloat() -> bool', 'isBool': 'isBool() -> bool', 'type': 'type -> string', 'typeInt': 'typeInt -> int — null → 0', 'typeFloat': 'typeFloat -> float — null → 0.0', 'typeBool': 'typeBool -> bool — null → false', 'typeString': 'typeString -> string — null → ""', 'typeArray': 'typeArray -> array — null → []', 'typeDict': 'typeDict -> dict — null → {}', 'typeTuple': 'typeTuple -> tuple — null → (,)', 'typeRange': 'typeRange -> range — null → 0..0', 'orElse': 'orElse(valorPorDefecto) -> any — devuelve valorPorDefecto (porque soy null)', 'isPresent': 'isPresent() -> bool — siempre false para null', } for _name, _sig in _sigs.items(): if _name in methods: methods[_name]['sig'] = _sig methods['method'] = {'cat': 'first', 'fn': lambda v, args, _m=methods: _describe_core_type_methods('null', _m)} return methods def _core_tuple_methods(): methods = { 'length': {'cat': 'first', 'fn': lambda v, args: len(v) if isinstance(v, tuple) else 0}, 'isEmpty': {'cat': 'first', 'fn': lambda v, args: len(v) == 0 if isinstance(v, tuple) else True}, 'isArray': {'cat': 'first', 'fn': lambda v, args: False}, 'isString':{'cat': 'first', 'fn': lambda v, args: False}, 'isInt': {'cat': 'first', 'fn': lambda v, args: False}, 'isFloat': {'cat': 'first', 'fn': lambda v, args: False}, 'isBool': {'cat': 'first', 'fn': lambda v, args: False}, 'type': {'cat': 'first', 'fn': lambda v, args: 'tuple'}, 'first': {'cat': 'first', 'fn': lambda v, args: v[0] if isinstance(v, tuple) and v else None}, 'last': {'cat': 'first', 'fn': lambda v, args: v[-1] if isinstance(v, tuple) and v else None}, 'typeString': {'cat': 'chainable', 'fn': lambda v, args: str(v)}, # ── AGREGADO: mismas consultas de lectura que ya tiene array ────── 'contains': {'cat': 'chainable', 'fn': lambda v, args: bool(args) and args[0] in v if isinstance(v, tuple) else False}, 'indexOf': {'cat': 'chainable', 'fn': lambda v, args: v.index(args[0]) if isinstance(v, tuple) and args and args[0] in v else -1}, 'toArray': {'cat': 'chainable', 'fn': lambda v, args: list(v) if isinstance(v, tuple) else v}, 'equals': {'cat': 'chainable', 'fn': lambda v, args: bool(args) and v == args[0]}, # ── AGREGADO: .kinds — misma idea que en array, pero devuelve una # tupla (mismo tipo de contenedor de entrada) con el tipo de cada # elemento, en el mismo orden. Shallow, igual que en array. 'kinds': {'cat': 'chainable', 'fn': lambda v, args: tuple(_get_value_type(x) for x in v) if isinstance(v, tuple) else v}, } _sigs = { 'length': 'length() -> int', 'isEmpty': 'isEmpty() -> bool', 'isArray': 'isArray() -> bool', 'isString': 'isString() -> bool', 'isInt': 'isInt() -> bool', 'isFloat': 'isFloat() -> bool', 'isBool': 'isBool() -> bool', 'type': 'type() -> string', 'first': 'first() -> any', 'last': 'last() -> any', 'typeString': 'typeString() -> string', 'contains': 'contains(valor) -> bool', 'indexOf': 'indexOf(valor) -> int', 'toArray': 'toArray() -> array', 'equals': 'equals(otraTupla) -> bool', 'kinds': 'kinds() -> tuple — tipo de cada elemento (shallow), ej. (21,3.14,"x").kinds() -> (int,float,string)', } for _name, _sig in _sigs.items(): if _name in methods: methods[_name]['sig'] = _sig methods['method'] = {'cat': 'first', 'fn': lambda v, args, _m=methods: _describe_core_type_methods('tuple', _m)} return methods def _core_dict_methods(): import copy methods = { 'length': {'cat': 'first', 'fn': lambda v, args: len(v) if isinstance(v, dict) else 0}, 'isEmpty': {'cat': 'first', 'fn': lambda v, args: len(v) == 0 if isinstance(v, dict) else True}, 'isArray': {'cat': 'first', 'fn': lambda v, args: False}, 'isString':{'cat': 'first', 'fn': lambda v, args: False}, 'isInt': {'cat': 'first', 'fn': lambda v, args: False}, 'isFloat': {'cat': 'first', 'fn': lambda v, args: False}, 'isBool': {'cat': 'first', 'fn': lambda v, args: False}, 'type': {'cat': 'first', 'fn': lambda v, args: 'dict'}, 'keys': {'cat': 'chainable', 'fn': lambda v, args: list(v.keys()) if isinstance(v, dict) else []}, 'values': {'cat': 'chainable', 'fn': lambda v, args: list(v.values()) if isinstance(v, dict) else []}, 'items': {'cat': 'chainable', 'fn': lambda v, args: [[k, vv] for k, vv in v.items()] if isinstance(v, dict) else []}, 'typeString': {'cat': 'chainable', 'fn': lambda v, args: str(v)}, # ── AGREGADO: consultas (inmutables — necesitan .mut, aunque no # tenga sentido mutar con estas, se deja consistente con el # patrón general del lenguaje) ───────────────────────────────── 'has': {'cat': 'chainable', 'fn': lambda v, args: bool(args) and args[0] in v if isinstance(v, dict) else False}, 'containsValue':{'cat': 'chainable', 'fn': lambda v, args: bool(args) and args[0] in v.values() if isinstance(v, dict) else False}, 'get': {'cat': 'chainable', 'fn': lambda v, args: v.get(args[0], args[1] if len(args) > 1 else None) if isinstance(v, dict) and args else None}, 'merge': {'cat': 'chainable', 'fn': lambda v, args: {**v, **args[0]} if isinstance(v, dict) and args and isinstance(args[0], dict) else v}, 'clone': {'cat': 'chainable', 'fn': lambda v, args: copy.deepcopy(v) if isinstance(v, dict) else v}, 'equals': {'cat': 'chainable', 'fn': lambda v, args: bool(args) and v == args[0]}, # ── AGREGADO: mutables por defecto (mismo manejo que push/pop en # array — no necesitan .mut para que el cambio se guarde) ────── 'remove': {'cat': 'chainable', 'mutable_default': True, 'fn': lambda v, args: {k: vv for k, vv in v.items() if k != args[0]} if isinstance(v, dict) and args else v}, 'set': {'cat': 'chainable', 'mutable_default': True, 'fn': lambda v, args: {**v, args[0]: args[1]} if isinstance(v, dict) and len(args) >= 2 else v}, 'clear': {'cat': 'chainable', 'mutable_default': True, 'fn': lambda v, args: {}}, # ── AGREGADO: .kinds — mismo dict (mismas claves) pero con cada # VALOR reemplazado por su tipo. Las claves nunca se tocan. # Shallow, igual que en array/tuple. 'kinds': {'cat': 'chainable', 'fn': lambda v, args: {k: _get_value_type(vv) for k, vv in v.items()} if isinstance(v, dict) else v}, } _sigs = { 'length': 'length() -> int', 'isEmpty': 'isEmpty() -> bool', 'isArray': 'isArray() -> bool', 'isString': 'isString() -> bool', 'isInt': 'isInt() -> bool', 'isFloat': 'isFloat() -> bool', 'isBool': 'isBool() -> bool', 'type': 'type() -> string', 'keys': 'keys() -> array', 'values': 'values() -> array', 'items': 'items() -> array', 'typeString': 'typeString() -> string', 'has': 'has(clave) -> bool', 'containsValue': 'containsValue(valor) -> bool', 'get': 'get(clave, porDefecto?) -> any', 'merge': 'merge(otroDict) -> dict', 'clone': 'clone() -> dict', 'equals': 'equals(otroDict) -> bool', 'remove': 'remove(clave) -> dict', 'set': 'set(clave, valor) -> dict', 'clear': 'clear() -> dict', 'kinds': 'kinds() -> dict — mismas claves, cada valor reemplazado por su tipo (shallow)', } for _name, _sig in _sigs.items(): if _name in methods: methods[_name]['sig'] = _sig methods['method'] = {'cat': 'first', 'fn': lambda v, args, _m=methods: _describe_core_type_methods('dict', _m)} return methods # Mapa global tipo → tabla de métodos def _binary_value(v, args) -> str: """ Convierte un valor escalar (int, float, str) a binario de n_bytes bytes. binary(n_bytes=1, signed=False) No muta el valor original. """ import struct as _struct n_bytes = int(args[0]) if args else 1 signed = bool(args[1]) if len(args) > 1 else False bits = n_bytes * 8 try: if isinstance(v, bool): return format(int(v), f'0{bits}b')[-bits:] if isinstance(v, int): i_val = v if signed and i_val < 0: i_val = i_val & ((1 << bits) - 1) return format(i_val, f'0{bits}b')[-bits:] if isinstance(v, float): raw = _struct.pack('>d', v) # 8 bytes IEEE 754 big-endian padded = raw[:n_bytes].ljust(n_bytes, b'\x00') return ''.join(f'{b:08b}' for b in padded) if isinstance(v, str): raw = v.encode('utf-8') padded = raw[:n_bytes].ljust(n_bytes, b'\x00') return ''.join(f'{b:08b}' for b in padded) except Exception: pass return '0' * bits _CORE_TYPE_METHODS = { 'string': _core_string_methods(), 'int': _core_int_methods(), 'float': _core_float_methods(), 'bool': _core_bool_methods(), 'array': _core_array_methods(), 'null': _core_null_methods(), 'tuple': _core_tuple_methods(), 'dict': _core_dict_methods(), } # ============================================================================== # RANGE SYSTEM # ============================================================================== import decimal as _decimal def _printable_or_square(c: str) -> str: """ AGREGADO: un carácter tal cual si es imprimible, o ▯ si no lo es (controles C0/C1, formato, etc. — mismo criterio que _quote_for_display, pero SIN comillas, para usarse en displays de rango tipo Range<...> o start..end donde los elementos no van entrecomillados). """ return c if c.isprintable() else '▯' class RangeValue: """ Tipo 'range' del lenguaje. Representa un rango perezoso [start..end]. Tipos soportados: int, float (1 decimal), string/unicode, null. Reglas: - start <= end (siempre) - Tipos homogéneos excepto int+float (se promociona a float) - null..null → rango nulo - Paso (step) por defecto: 1 para int/string, 0.1 para float """ _KIND_INT = 'int' _KIND_FLOAT = 'float' _KIND_STRING = 'string' _KIND_NULL = 'null' def __init__(self, start, end, step=None, half_open: bool = False): self.start = start self.end = end self.half_open = half_open # True → excluye end (1..<5 = 1,2,3,4) self._kind = self._infer_kind(start, end) self._step = step # ── Inferencia de tipo ──────────────────────────────────────────────────── @staticmethod def _infer_kind(start, end): if start is None and end is None: return RangeValue._KIND_NULL if isinstance(start, float) or isinstance(end, float): return RangeValue._KIND_FLOAT if isinstance(start, int) and isinstance(end, int): return RangeValue._KIND_INT if isinstance(start, str) and isinstance(end, str): return RangeValue._KIND_STRING return RangeValue._KIND_INT @property def kind(self): return self._kind # ── Paso efectivo ───────────────────────────────────────────────────────── @property def effective_step(self): if self._step is not None: return self._step return 0.1 if self._kind == self._KIND_FLOAT else 1 # ── Expansión bajo demanda ──────────────────────────────────────────────── def expand(self) -> list: """Genera la lista de valores del rango.""" if self._kind == self._KIND_NULL: return [] if self._kind == self._KIND_INT: end = self.end if not self.half_open else self.end - 1 return list(range(self.start, end + 1, max(1, int(self.effective_step)))) if self._kind == self._KIND_FLOAT: import decimal as _dec step = _dec.Decimal(str(self.effective_step)) cur = _dec.Decimal(str(self.start)) stop = _dec.Decimal(str(self.end)) if self.half_open: # excluir end result = [] while cur < stop: result.append(float(cur)) cur += step return result result = [] while cur <= stop: result.append(float(cur)) cur += step if result and _dec.Decimal(str(result[-1])) < stop: result.append(float(stop)) return result if self._kind == self._KIND_STRING: s_cp = ord(self.start) e_cp = ord(self.end) if not self.half_open else ord(self.end) - 1 step = max(1, int(self.effective_step)) return [chr(cp) for cp in range(s_cp, e_cp + 1, step)] return [] # ── Longitud ────────────────────────────────────────────────────────────── def length(self) -> int: return len(self.expand()) # ── Representación ──────────────────────────────────────────────────────── def format_display(self) -> str: """Range<0,1,2,3>""" if self._kind == self._KIND_NULL: return 'Range' items = self.expand() # REPARADO: en un rango de string, un carácter no-imprimible # (ej. los controles C1 \u0080-\u009f) quedaba invisible entre # las comas — ahora se muestra como ▯ para que se note que hay # un elemento ahí. if self._kind == self._KIND_STRING: inner = ','.join(_printable_or_square(v) for v in items) else: inner = ','.join(str(v) for v in items) return f'Range<{inner}>' def format_short(self) -> str: """start..end (forma compacta)""" if self._kind == self._KIND_NULL: return 'null..null' # REPARADO: mismo caso que format_display, pero para los extremos # del rango en su forma compacta (ej. en mensajes de error o # .typeString()). s = f'"{_printable_or_square(self.start)}"' if self._kind == self._KIND_STRING else str(self.start) e = f'"{_printable_or_square(self.end)}"' if self._kind == self._KIND_STRING else str(self.end) sep = '..<' if self.half_open else '..' return f'{s}{sep}{e}' def __repr__(self): return f'RangeValue({self.format_short()})' # ── Pertenencia ('in' / 'not in') ────────────────────────────────────────── def __contains__(self, value): """ AGREGADO: soporte del operador 'in'/'not in' sobre un rango, ej. '5 in 1..10' o '"m" in "a".."z"'. Chequea los límites directamente (sin expandir la lista completa — importante para rangos grandes), respetando el tipo del rango (int/float/string) y si es semi-abierto (1..<10 excluye el 10). """ if self._kind == self._KIND_NULL: return False if self._kind == self._KIND_STRING: if not isinstance(value, str) or len(value) != 1: return False cp = ord(value) lo, hi = ord(self.start), ord(self.end) if self.half_open: return lo <= cp < hi return lo <= cp <= hi # int / float (bool queda excluido a propósito: es un tipo aparte) if isinstance(value, bool) or not isinstance(value, (int, float)): return False if self.half_open: return self.start <= value < self.end return self.start <= value <= self.end def _core_range_methods(): def _must_be_range(v): if not isinstance(v, RangeValue): raise Exception(f"Se esperaba un rango, se recibió {type(v).__name__}") def _get_start(v, args): _must_be_range(v); return v.start def _get_end(v, args): _must_be_range(v); return v.end def _get_length(v, args):_must_be_range(v); return v.length() def _set_step(v, args): _must_be_range(v) if not args: raise Exception("step() requiere un argumento numérico") try: new_step = float(args[0]) except (TypeError, ValueError): raise Exception(f"step() requiere número") return RangeValue(v.start, v.end, step=new_step, half_open=v.half_open) def _to_array(v, args): _must_be_range(v); return v.expand() def _to_tuple(v, args): _must_be_range(v); return tuple(v.expand()) def _type_fn(v, args): return 'range' def _is_empty(v, args): _must_be_range(v); return v.length() == 0 def _uni(v, args): """ Devuelve los valores unicode (codepoints) del rango como lista de strings. Para rangos string: en vez de caracteres, devuelve 'U+XXXX'. Para rangos int/float: devuelve la representación en memoria (hex del int/float). No muta. """ _must_be_range(v) items = v.expand() if v.kind == RangeValue._KIND_STRING: return [f'U+{ord(c):04X}' for c in items] elif v.kind == RangeValue._KIND_INT: return [hex(i) for i in items] elif v.kind == RangeValue._KIND_FLOAT: import struct return [hex(struct.unpack(' 1 else 'int' signed = bool(args[2]) if len(args) > 2 else False bits = n_bytes * 8 items = v.expand() result = [] for item in items: try: if isinstance(item, str): # UTF-8: cada byte como binario, pad hasta n_bytes raw = item.encode('utf-8') # Tomar hasta n_bytes, rellenar con ceros si es más corto padded = raw[:n_bytes].ljust(n_bytes, b'\x00') result.append(''.join(f'{b:08b}' for b in padded)) elif isinstance(item, float): import struct # Double IEEE 754 → 8 bytes, recortar/pad a n_bytes raw = struct.pack('>d', item) padded = raw[:n_bytes].ljust(n_bytes, b'\x00') result.append(''.join(f'{b:08b}' for b in padded)) else: i_val = int(item) if signed and i_val < 0: # Complemento a dos i_val = i_val & ((1 << bits) - 1) # format con el número exacto de bits (cero-padded) result.append(format(i_val, f'0{bits}b')[-bits:]) except Exception: result.append('0' * bits) return result def _contains(v, args): _must_be_range(v) if not args: return False return args[0] in v # usa RangeValue.__contains__ def _to_reversed_array(v, args): _must_be_range(v) return list(reversed(v.expand())) methods = { 'start': {'cat': 'first', 'fn': _get_start}, 'end': {'cat': 'first', 'fn': _get_end}, 'length': {'cat': 'first', 'fn': _get_length}, 'step': {'cat': 'chainable', 'fn': _set_step}, 'toarray': {'cat': 'chainable', 'fn': _to_array}, 'totuple': {'cat': 'chainable', 'fn': _to_tuple}, 'type': {'cat': 'first', 'fn': _type_fn}, 'isEmpty': {'cat': 'first', 'fn': _is_empty}, 'uni': {'cat': 'chainable', 'fn': _uni}, 'binary': {'cat': 'chainable', 'fn': _binary}, 'typeString': {'cat': 'chainable', 'fn': lambda v, a: v.format_short()}, # ── AGREGADO ─────────────────────────────────────────────────────── 'contains': {'cat': 'chainable', 'fn': _contains}, 'toReversedArray': {'cat': 'chainable', 'fn': _to_reversed_array}, } _sigs = { 'start': 'start() -> int|float', 'end': 'end() -> int|float', 'length': 'length() -> int', 'step': 'step(paso) -> range', 'toarray': 'toarray() -> array', 'totuple': 'totuple() -> tuple', 'type': 'type() -> string', 'isEmpty': 'isEmpty() -> bool', 'uni': 'uni() -> array', 'binary': 'binary(bytes?, tipo?, signed?) -> array', 'typeString': 'typeString() -> string', 'contains': 'contains(valor) -> bool', 'toReversedArray': 'toReversedArray() -> array', } for _name, _sig in _sigs.items(): if _name in methods: methods[_name]['sig'] = _sig methods['method'] = {'cat': 'first', 'fn': lambda v, args, _m=methods: _describe_core_type_methods('range', _m)} return methods _CORE_TYPE_METHODS['range'] = _core_range_methods() # ============================================================================== # RANGE PARSING HELPERS (module-level, usados desde el intérprete) # ============================================================================== _RANGE_LITERAL_RE = re.compile( r'^' r'(-?\d+\.\d+|-?\d+|' # int o float r'"[^"]*"|\'[^\']*\'|' # string con comillas dobles o simples r'null|' # null r'u[0-9A-Fa-f]{4,6})' # unicode literal u0041 r'\.\.' r'(-?\d+\.\d+|-?\d+|' r'"[^"]*"|\'[^\']*\'|' r'null|' r'u[0-9A-Fa-f]{4,6})' r'$' ) def _parse_range_endpoint(raw: str, half_open: bool = False): """ Convierte un endpoint de rango a su valor Python. Retorna (value, kind) donde kind ∈ {'int','float','string','null'}. Unicode SOLO entre comillas: "u0041" → chr(0x41) = 'A' Sin comillas: u0041 es un identificador, no unicode. """ raw = raw.strip() if raw == 'null': return None, 'null' # String entre comillas (incluyendo unicode entre comillas) if (raw.startswith('"') and raw.endswith('"')) or \ (raw.startswith("'") and raw.endswith("'")): s = raw[1:-1] # Unicode dentro de comillas: "u0041" → 'A' if re.fullmatch(r'u[0-9A-Fa-f]{4,6}', s): cp = int(s[1:], 16) return chr(cp), 'string' if len(s) != 1: raise ValueError( f"Endpoint de rango string debe ser un solo carácter, se recibió {raw!r}") return s, 'string' # Float if re.fullmatch(r'-?\d+\.\d+', raw): return float(raw), 'float' # Int if re.fullmatch(r'-?\d+', raw): return int(raw), 'int' raise ValueError(f"Endpoint de rango inválido: {raw!r}") def _build_range(raw_start: str, raw_end: str, half_open: bool = False) -> 'RangeValue': """ Construye y valida un RangeValue desde sus endpoints en texto. half_open=True → excluye el endpoint final (1..<5 = 1,2,3,4). Lanza ValueError con mensaje descriptivo si algo es inválido. """ s_val, s_kind = _parse_range_endpoint(raw_start) e_val, e_kind = _parse_range_endpoint(raw_end) # Rango nulo if s_kind == 'null' and e_kind == 'null': return RangeValue(None, None, half_open=half_open) if s_kind == 'null' or e_kind == 'null': raise ValueError("Solo se permite null..null, no null mezclado con otro tipo") # Promoción int→float if s_kind == 'float' and e_kind == 'int': e_val = float(e_val); e_kind = 'float' if e_kind == 'float' and s_kind == 'int': s_val = float(s_val); s_kind = 'float' if s_kind != e_kind: raise ValueError( f"Tipos incompatibles en rango: {s_kind} .. {e_kind}. " f"Solo se pueden combinar int+float.") # Validar start <= end (para half_open: start < end) if s_kind == 'string': limit = ord(e_val) - (1 if half_open else 0) if ord(s_val) > limit: raise ValueError( f"Rango inválido: el inicio '{s_val}' (U+{ord(s_val):04X}) " f"es mayor que el fin efectivo") else: limit = e_val - (1 if half_open and s_kind == 'int' else 0) if s_val > (e_val if not half_open or s_val == e_val else e_val - 1e-12): raise ValueError( f"Rango inválido: el inicio {s_val} es mayor que el fin {e_val}") # Validar flotante de 1 decimal if s_kind == 'float': def _check_decimal(v, name): s = str(v) if '.' in s and len(s.split('.')[1]) > 1: raise ValueError( f"Rango float: {name} '{v}' tiene más de 1 decimal. " f"Solo se permite 1 decimal (ej. 0.5..1.5)") _check_decimal(s_val, 'start') _check_decimal(e_val, 'end') # Validar string: no mezclar mayúsculas/minúsculas if s_kind == 'string': sl, el = s_val.islower(), e_val.islower() su, eu = s_val.isupper(), e_val.isupper() if (sl and eu) or (su and el): raise ValueError( f"Rango string: no se pueden combinar mayúsculas y minúsculas " f"('{s_val}'..'{e_val}')") return RangeValue(s_val, e_val, half_open=half_open) def _expand_collection_items(items: list) -> list: """ Expande RangeValues dentro de una lista de items (para arrays/tuplas). [1..3, 4, 5..6] → [1, 2, 3, 4, 5, 6] """ result = [] for item in items: if isinstance(item, RangeValue): result.extend(item.expand()) else: result.append(item) return result # Tipos nativos del lenguaje (actualizado con range) _NATIVE_TYPES = frozenset({ 'dynamic', 'int', 'float', 'string', 'bool', 'array', 'tuple', 'dict', 'null', 'NULL', 'any', 'range' }) def _get_value_type(value) -> str: """Infiere el tipo Tesseract de un valor Python.""" if value is None: return 'null' if isinstance(value, RangeValue): return 'range' if isinstance(value, bool): return 'bool' if isinstance(value, int): return 'int' if isinstance(value, float): return 'float' if isinstance(value, str): return 'string' if isinstance(value, tuple): return 'tuple' if isinstance(value, list): return 'array' if isinstance(value, dict): return 'dict' return 'dynamic' # ============================================================================== # Coerción implícita de tipos numéricos/booleanos — AGREGADO # ============================================================================== # Replica la conversión implícita estilo C/Java entre tipos "afines": # - int ← float : trunca la parte decimal (2.5 → 2) # - float ← int : promueve a float (2 → 2.0) # - int ← bool : true → 1, false → 0 # - bool ← int : 0 → false, 1 → true (cualquier otro entero sigue # siendo un conflicto de tipo, no se coacciona) # No aplica a ninguna otra combinación de tipos (esas siguen siendo un # conflicto de tipo, exactamente igual que antes). def _coerce_scalar_value(value, target_type): """ Intenta convertir un valor hoja `value` al `target_type` declarado. Devuelve (valor_resultante, changed: bool). Si `changed` es False, `valor_resultante` es el mismo `value` sin modificar (no hubo coerción aplicable y el llamador debe tratarlo como conflicto de tipo). """ if target_type == 'int': if isinstance(value, bool): return (1 if value else 0), True if isinstance(value, float): return int(value), True return value, False if target_type == 'float': if isinstance(value, bool): return value, False if isinstance(value, int): return float(value), True return value, False if target_type == 'bool': if isinstance(value, bool): return value, False if isinstance(value, int) and value in (0, 1): return bool(value), True return value, False return value, False # ============================================================================== # Colecciones tipadas — AGREGADO # ============================================================================== # Cuando una variable se declara con un tipo escalar/nativo (int, float, # string, bool, ...) pero se le asigna un array, tupla o dict, ese tipo pasa # a interpretarse como el tipo que debe cumplir CADA elemento de la colección, # de forma recursiva (incluyendo colecciones anidadas dentro de ella). # # Ejemplo: # int arr = [1, 2, 3, {"val1":1,"val2":5,"val3":2}, (5,7,8), [1,2,6]]; # → todo valor "hoja" (no-colección) dentro de arr debe ser 'int'. # # Esto NO reemplaza el sistema de verificación simple ya existente; sólo se # activa cuando el tipo declarado es escalar y el valor real es una colección. def _coerce_collection_element_types(value, elem_type): """ Recorre recursivamente `value` (que puede ser list/tuple/dict o un valor simple) y construye una copia donde cada elemento hoja se coacciona al tipo `elem_type` (misma coerción implícita int↔float↔bool que aplica a variables escalares, ver `_coerce_scalar_value`). Devuelve una tupla (ok, coerced_value, bad_value, bad_type): - ok=True, coerced_value → todo cumple/se coaccionó correctamente. - ok=False, None, bad_value, bad_type → el primer elemento que no cumple ni se puede coaccionar, y su tipo real. """ if isinstance(value, list): result = [] for item in value: ok, coerced, bad_value, bad_type = _coerce_collection_element_types(item, elem_type) if not ok: return False, None, bad_value, bad_type result.append(coerced) return True, result, None, None if isinstance(value, tuple): result = [] for item in value: ok, coerced, bad_value, bad_type = _coerce_collection_element_types(item, elem_type) if not ok: return False, None, bad_value, bad_type result.append(coerced) return True, tuple(result), None, None if isinstance(value, dict): # Sólo se coacciona/valida el tipo de los VALORES del dict, no de sus claves. result = {} for k, v in value.items(): ok, coerced, bad_value, bad_type = _coerce_collection_element_types(v, elem_type) if not ok: return False, None, bad_value, bad_type result[k] = coerced return True, result, None, None # ── Valor hoja ───────────────────────────────────────────────────────── if value is None: return True, None, None, None # null se permite dentro de la colección actual = _get_value_type(value) if actual == elem_type: return True, value, None, None coerced, changed = _coerce_scalar_value(value, elem_type) if changed: return True, coerced, None, None return False, None, value, actual def _quote_for_display(s: str) -> str: """ AGREGADO: comilla un string para mostrarlo como ELEMENTO de una colección (array/tupla/dict), SIN escapar caracteres no-ASCII a la forma \\uXXXX de json.dumps (esa forma sólo debe salir cuando se pide explícitamente con el core method .uni, no en un print normal). Los caracteres genuinamente no imprimibles (controles C0/C1, etc. — cualquiera para el que Python considere str.isprintable() == False) se muestran como ▯ en vez de quedar invisibles o convertirse en un escape ilegible. """ out = ['"'] for c in s: if c == '"': out.append('\\"') elif c == '\\': out.append('\\\\') elif c == '\n': out.append('\\n') elif c == '\t': out.append('\\t') elif c == '\r': out.append('\\r') elif not c.isprintable(): out.append('▯') else: out.append(c) out.append('"') return ''.join(out) def _fmt_val(v, quote_str: bool = False) -> str: """ Formatea un valor Python al estilo del lenguaje. AGREGADO: parámetro quote_str — cuando el valor a formatear es un string y quote_str=True, se envuelve en comillas dobles (con los escapes correspondientes vía json.dumps). Se usa para que los elementos STRING dentro de un array/tupla/dict se muestren entrecomillados (para no confundirlos con otros tipos), mientras que un string mostrado "suelto" (top-level, o al concatenar con '.') sigue mostrándose SIN comillas, igual que siempre. Los elementos de list/tuple/dict que se formatean recursivamente aquí adentro SIEMPRE se piden con quote_str=True — así una colección anidada dentro de otra colección también entrecomilla sus strings. Las CLAVES del dict nunca se tocan (sólo los valores). """ if v is None: return 'null' if isinstance(v, RangeValue): return v.format_display() if isinstance(v, bool): return 'true' if v else 'false' if isinstance(v, float): s = f'{v:.15f}'.rstrip('0') return s if not s.endswith('.') else s + '0' if isinstance(v, tuple): return '(' + ', '.join(_fmt_val(i, True) for i in v) + ')' if isinstance(v, list): return '[' + ', '.join(_fmt_val(i, True) for i in v) + ']' if isinstance(v, dict): return '{' + ', '.join(f"{k}:{_fmt_val(vv, True)}" for k, vv in v.items()) + '}' if isinstance(v, str): return _quote_for_display(v) if quote_str else v return str(v) def _value_matches_declared_type(value, declared_type: str) -> bool: """ AGREGADO: chequeo simple de compatibilidad de tipo, usado únicamente para validar la ÚNICA asignación permitida sobre una constante que fue declarada con valor null (ver SymbolTable.set_value). No reemplaza ni toca _validate_declared_type_value (la validación completa que usa handle_VariableDeclaration/handle_ConstantDeclaration en el momento de declarar) — es deliberadamente más simple porque aquí sólo se necesita decidir si el valor de la asignación calza con el tipo ya declarado. """ if declared_type in ('dynamic', 'any', None): return True if value is None: # null NUNCA es un valor válido para la asignación de una # constante TIPADA — null sólo se permite en la DECLARACIÓN. return False vtype = _get_value_type(value) if vtype == declared_type: return True # Coerción numérica int<->float, igual que el resto del lenguaje if declared_type in ('int', 'float') and vtype in ('int', 'float'): return True return False # ============================================================================== # Tabla de Símbolos (Sin cambios) # ============================================================================== class SymbolTable: def __init__(self): self.symbols = [{}] self.function_symbols = {} self.function_params = {} self.function_param_values = {} self.scope_history = {} self._context_stack = [] # stack de labels activos @staticmethod def _normalize_value(value): if isinstance(value, float): return float(f'{value:.15g}'.rstrip('0').rstrip('.') or '0') return value def push_scope(self, label=""): """Crea un nuevo scope local.""" self.symbols.append({}) self._context_stack.append(label) def pop_scope(self, label=""): if len(self.symbols) > 1: scope = self.symbols[-1] if scope: ctx = self._context_stack[:-1] # contexto padre path = " > ".join(c for c in ctx if c) lbl = f"{path} > {label}" if path else label self.scope_history[lbl] = {k: v.value for k, v in scope.items()} if self._context_stack: self._context_stack.pop() self.symbols.pop() def _debug_log(self, message): """Log interno de la tabla, activado desde el intérprete.""" if getattr(self, '_debug_mode', False): print(message) def declare(self, name, symbol): """Declara un nuevo símbolo.""" self.symbols[-1][name] = symbol def set_value(self, name, value): for scope in reversed(self.symbols): if name in scope: sym = scope[name] if sym.is_const: # Caso: const con valor null → permite UNA asignación if sym.value is None and not sym._null_assigned: # REPARADO: la única asignación permitida debe # respetar el tipo declarado cuando la constante es # TIPADA (ej. 'const int x = null;'). null NUNCA es # un valor válido para esa asignación en ese caso — # null sólo se permitía en la declaración. Para una # constante DINÁMICA (declared_type == 'dynamic'), # cualquier valor —incluido null— es válido. if not _value_matches_declared_type(value, sym.declared_type): _got = 'null' if value is None else _get_value_type(value) raise TypeExplicitConflictError( f"No se puede asignar un valor de tipo '{_got}' a la " f"constante '{name}': fue declarada con tipo " f"'{sym.declared_type}', y esta es su única asignación " f"permitida (ya tenía valor null)." ) # REPARADO: cualquier asignación —incluido null— # cuenta como la única permitida. Antes, si el valor # asignado era null, NO se marcaba _null_assigned, # permitiendo reasignar indefinidamente mientras se # siguiera asignando null; ahora null también fija # el valor final y bloquea asignaciones futuras. sym._null_assigned = True sym.value = value return # Ya tiene valor no-null o ya fue asignada → error raise ConstError( msg("var.const_reassign", name=name) ) sym.value = value return raise NameError( msg("name.undefined", name=name) ) def get_value(self, name): """Obtiene el valor actual de un símbolo.""" for scope in reversed(self.symbols): if name in scope: return scope[name].value if str(name).lower() == 'true': return True if str(name).lower() == 'false': return False try: return int(name) except ValueError: try: return float(name) except ValueError: raise UndeclaredVariableError(name) def push_function_frame(self, func_name): """Crea un nuevo frame para una llamada recursiva.""" if func_name not in self.function_param_values: self.function_param_values[func_name] = [] self.function_param_values[func_name].append({}) def pop_function_frame(self, func_name): """Elimina el frame actual al retornar de la función.""" if func_name in self.function_param_values and self.function_param_values[func_name]: self.function_param_values[func_name].pop() def get_symbol(self, name): """Obtiene el objeto Symbol completo.""" for scope in reversed(self.symbols): if name in scope: return scope[name] raise UndeclaredVariableError(name) def declare_function(self, name, function_node): """Declara una nueva función.""" self.function_symbols[name] = function_node def declare_function_params(self, func_name, params): """Declara los parámetros de una función.""" self.function_params[func_name] = params def get_function(self, name): """Obtiene la definición de una función.""" if name not in self.function_symbols: raise UndeclaredVariableError(f"Función '{name}' no ha sido declarada.") return self.function_symbols[name] def get_function_params(self, func_name): """Obtiene los parámetros de una función.""" return self.function_params.get(func_name, {}) def declare_function_param_value(self, func_name, param_name, value): """Guarda el valor de un parámetro para una función específica""" if func_name not in self.function_param_values: self.function_param_values[func_name] = [{}] self.function_param_values[func_name][-1][param_name] = value def get_function_param_value(self, func_name, param_name): """Obtiene el valor de un parámetro de una función""" if (func_name in self.function_param_values and self.function_param_values[func_name] and param_name in self.function_param_values[func_name][-1]): return self.function_param_values[func_name][-1][param_name] raise UndeclaredVariableError(f"Parámetro '{param_name}' no tiene valor en función '{func_name}'") def __str__(self): # Variables normales def _fmt(v): if isinstance(v, float): s = f'{v:.15f}'.rstrip('0') return s if not s.endswith('.') else s + '0' return v scopes_list = [] for i, scope in enumerate(self.symbols): label = "Global" if i == 0 else f"Local {i}" scopes_list.append(f"{label}: { {k: _fmt(v.value) for k, v in scope.items()} }") symbols_str = f"Variables:\n " + "\n ".join(scopes_list) # Funciones declaradas functions_str = f"Funciones: {list(self.function_symbols.keys())}" # Parámetros CON SUS VALORES CORRECTOS params_with_values = {} for func_name, params_info in self.function_params.items(): func_params = {} for param_name, param_type in params_info.items(): # Buscar en la tabla específica de valores de parámetros if (func_name in self.function_param_values and param_name in self.function_param_values[func_name]): param_value = self.function_param_values[func_name][param_name] func_params[param_name] = f"{param_type} = {param_value}" else: func_params[param_name] = f"{param_type} = " params_with_values[func_name] = func_params params_str = f"Parámetros: {params_with_values}" history_str = "" if self.scope_history: history_lines = ["Variables Locales (historial):"] for lbl, vars in self.scope_history.items(): history_lines.append(f" [{lbl}]: {vars}") history_str = "\n " + "\n ".join(history_lines) return f"Tabla de Símbolos:\n {symbols_str}\n {functions_str}\n {params_str}{history_str}" #return f"Tabla de Símbolos:\n {symbols_str}\n {functions_str}\n {params_str}" # ============================================================================== # Intérprete Principal # ============================================================================== # ────────────────────────────────────────────────────────────────────────── # Funciones nativas del núcleo del lenguaje (__dunder__) que NO pasan por # SymbolTable.function_symbols (esa tabla es solo para funciones declaradas # por el usuario con 'function'/'perform'). Cualquier función nativa nueva # de este estilo (config.tsc, capacidades, etc.) debe registrarse aquí para # que FunctionContext.execute_function() y _resolve_embedded_function_calls # no la reporten como "func.undefined". El valor es el nombre del método # correspondiente en la clase Interpreter, que debe aceptar cero argumentos. NATIVE_CORE_FUNCTIONS = { '__tesseract_config': '_builtin_tesseract_config', '__tesseract_capabilities': '_builtin_tesseract_capabilities', } class FunctionContext: """Contexto para ejecución de funciones con su propia tabla de símbolos temporal""" def __init__(self, interpreter, function_name, parameters, call_node=None): self.interpreter = interpreter self.function_name = function_name self.parameters = parameters self.local_symbols = {} # Nodo AST de la llamada (CallExpression/FunctionCall), si está disponible. # Permite anclar el cursor '^^^' al argumento exacto que falla en vez # de a self.interpreter._current_line/_current_col (la línea completa). self.call_node = call_node def set_parameter_value(self, param_name, value): """Establece el valor de un parámetro en el contexto local""" self.local_symbols[param_name] = value def get_parameter_value(self, param_name): """Obtiene el valor de un parámetro del contexto local""" return self.local_symbols.get(param_name) def _lc_for_arg(self, text): """ Calcula file/line/col/function para anclar el cursor del traceback al texto de un argumento específico de esta llamada. Si el valor es un string, primero intenta buscarlo con comillas dobles ("valor") para que el cursor apunte al literal completo tal como aparece en el código fuente; si no encuentra, busca sin comillas. Para otros tipos, convierte a string y busca directamente. Usa call_node si está disponible (vía interpreter._lc_at); si no, cae a _lc_here — igual que el comportamiento previo a este cambio. """ if isinstance(text, str): # Intentar primero con comillas (el literal tal cual en el source) quoted = f'"{text}"' if self.call_node is not None: result = self.interpreter._lc_at(self.call_node, quoted) if result.get("col") != self.call_node.get("linecolumn", {}).get("col"): return result # Si col no cambio (no encontro el quoted), intentar sin comillas return self.interpreter._lc_at(self.call_node, text) else: result = self.interpreter._lc_here(quoted) base_col = self.interpreter._lc().get("col") if result.get("col") != base_col: return result return self.interpreter._lc_here(text) else: text_str = str(text) if text is not None else "" if self.call_node is not None: return self.interpreter._lc_at(self.call_node, text_str) return self.interpreter._lc_here(text_str) def execute_function(self): # ── Funciones nativas del núcleo (__tesseract_config, __tesseract_capabilities, ...) ── # Estas no viven en symbol_table.function_symbols (esa tabla es solo para # funciones declaradas por el usuario), así que deben interceptarse aquí # antes de que get_function() las reporte como "func.undefined". if self.function_name in NATIVE_CORE_FUNCTIONS: _received = len(self.parameters) if self.parameters else 0 if _received != 0: raise ArityError( msg("func.arity", name=self.function_name, expected=0, got=_received), **self._lc_for_arg(self.function_name) ) _native_method = getattr(self.interpreter, NATIVE_CORE_FUNCTIONS[self.function_name]) return _native_method() try: function_def = self.interpreter.symbol_table.get_function(self.function_name) except UndeclaredVariableError: raise UndefinedFunctionError( msg("func.undefined", name=self.function_name), file=self.interpreter._source_file, function=self.function_name, line=self.interpreter._current_line, col=self.interpreter._current_col, ) self.interpreter.symbol_table.push_scope(label=f"Función '{self.function_name}'") function_params = self.interpreter.symbol_table.get_function_params(self.function_name) # ── Verificar aridad ────────────────────────────────────────────────── _expected = len(function_params) _received = len(self.parameters) if self.parameters else 0 if _expected != _received: raise ArityError( msg("func.arity", name=self.function_name, expected=_expected, got=_received), **self._lc_for_arg(self.function_name) ) # ASIGNAR PARÁMETROS A LA TABLA ESPECÍFICA self.interpreter.symbol_table.push_function_frame(self.function_name) self._assign_received_parameters(function_params) # Validar tipos de parámetros si están tipados (int, string, float, etc.) self._validate_parameter_types(function_params) old_function = self.interpreter._current_function # guardar la función padre # Establecer función actual self.interpreter._current_function = self.function_name # Ejecutar bloque if "block" in function_def: # ── Push en call_stack para traceback ──────────────────────────── _fn_line = getattr(function_def, 'get', lambda k, d=None: d)('linecolumn', {}).get('line') if isinstance(function_def, dict) else None self.interpreter._call_stack.append( (self.interpreter._source_file, self.function_name, _fn_line) ) self.interpreter._log(f"-> Ejecutando función '{self.function_name}'") old_break_flag = self.interpreter.break_flag result = None self.interpreter.return_flag = False self.interpreter.return_value = None for statement in function_def["block"]: if self.interpreter.break_flag or self.interpreter.return_flag: break self.interpreter.execute_node(statement) if self.interpreter.return_flag: result = self.interpreter.return_value break # Limpiar el flag al salir de la función self.interpreter.return_flag = False self.interpreter.return_value = None self.interpreter.break_flag = old_break_flag self.interpreter._current_function = old_function self.interpreter.symbol_table.pop_scope(label=f"Función '{self.function_name}'") self.interpreter.symbol_table.pop_function_frame(self.function_name) # ── Pop del call_stack al retornar ────────────────────────────── if self.interpreter._call_stack: self.interpreter._call_stack.pop() self.interpreter._log(f"<- Finalizada función '{self.function_name}'") return result self.interpreter._current_function = old_function self.interpreter.symbol_table.pop_scope(label=f"Función '{self.function_name}'") self.interpreter.symbol_table.pop_function_frame(self.function_name) return None def _assign_received_parameters(self, function_params): param_names = list(function_params.keys()) for i, param_name in enumerate(param_names): if i < len(self.parameters): param_value = self.parameters[i] # GUARDAR EN TABLA ESPECÍFICA DE PARÁMETROS self.interpreter.symbol_table.declare_function_param_value( self.function_name, param_name, param_value ) self.interpreter._log(f" Asignado parámetro '{param_name}': {param_value}") def _validate_parameter_types(self, function_params): """Valida tipos de parámetros tipados. Solo actúa si el param tiene tipo != any/dynamic.""" for i, (param_name, expected_type) in enumerate(function_params.items()): if expected_type in ('any', 'dynamic', '', None): continue # inferido o dinámico — no validar if i >= len(self.parameters): continue # aridad ya validada antes param_value = self.parameters[i] actual_type = self._get_type_name(param_value) # Permitir promoción int → float if expected_type == 'float' and actual_type == 'int': continue if actual_type != expected_type: raise ArgumentTypeError( msg("func.arg_type", param=param_name, func=self.function_name, expected=expected_type, got=actual_type), **self._lc_for_arg(param_value) ) def _get_type_name(self, value): """ Obtiene el nombre del tipo de un valor, para validar parámetros tipados al llamar una función (ver _validate_parameter_types). REPARADO: antes sólo reconocía bool/int/float/string y todo lo demás (arrays, tuplas, dicts, null...) caía en 'any' por defecto — así que un parámetro declarado como 'array' SIEMPRE fallaba la validación de tipo (o se saltaba de forma incorrecta) porque el argumento recibido se detectaba como 'any' en vez de 'array'. Ahora delega a _get_value_type, la función canónica de tipos que ya usa el resto del intérprete (declared_type de variables, .type de core methods, etc.), para que el tipado de parámetros quede 100% consistente con el resto del lenguaje. """ return _get_value_type(value) def _execute_block_with_context(self, block): """Ejecuta un bloque usando el contexto local de la función""" # Para cada nodo en el bloque for statement in block: if self.interpreter.break_flag: break node_type = list(statement.keys())[0] node_content = statement[node_type] # Manejar asignación de parámetros de forma especial if node_type == "ParameterAsignement": self._handle_parameter_assignment(node_content) else: # Para otros nodos, usar el ejecutor normal pero con contexto local self.interpreter.execute_node(statement) def _handle_parameter_assignment(self, node): """Maneja la asignación de parámetros dentro de la función""" param_name = node["name"] value_node = node.get("value", {}) # Evaluar el valor a asignar if "operation" in value_node: operation_node = value_node["operation"] expression_str = operation_node.get("value") if isinstance(operation_node, dict) else operation_node final_value = self.interpreter.resolve_expression(expression_str) else: raw_value = value_node.get("value") final_value = raw_value # Validar tipo si el parámetro está tipado function_params = self.interpreter.symbol_table.get_function_params(self.function_name) if param_name in function_params and function_params[param_name] != "any": expected_type = function_params[param_name] actual_type = self._get_type_name(final_value) if expected_type == 'float' and actual_type == 'int': pass # promoción permitida elif expected_type != actual_type: raise ArgumentTypeError( msg("func.arg_type", param=param_name, func=self.function_name, expected=expected_type, got=actual_type), **self._lc_for_arg(final_value) ) # Asignar el valor al parámetro en el contexto local self.set_parameter_value(param_name, final_value) self.interpreter._log(f" Asignado parámetro '{param_name}': {final_value}") # ============================================================================== # OOP — Contexto de ejecución de métodos # ============================================================================== class MethodContext: """Equivalente a FunctionContext pero para métodos de clase con acceso a 'this'.""" def __init__(self, interpreter, method_info: dict, instance: ClassInstance, class_origin: str, parameters: list): self.interpreter = interpreter self.method_info = method_info # {node, params, modifier, is_async} self.instance = instance self.class_origin = class_origin self.parameters = parameters def execute(self): interp = self.interpreter method_node = self.method_info['node'] method_name = method_node.get('name', '') func_params = self.method_info.get('params', {}) frame_key = f"{self.class_origin}.{method_name}" interp._log(f"[OOP] -> Método '{method_name}' en '{self.class_origin}' args={self.parameters}") # Guardar contexto previo old_function = interp._current_function old_instance = interp._current_instance interp._current_function = frame_key interp._current_instance = self.instance interp.symbol_table.push_function_frame(frame_key) interp.symbol_table.push_scope(label=f"Método '{method_name}'") # Asignar parámetros param_names = list(func_params.keys()) for i, pname in enumerate(param_names): if i < len(self.parameters): interp.symbol_table.declare_function_param_value( frame_key, pname, self.parameters[i] ) interp._log(f"[OOP] param '{pname}' = {self.parameters[i]}") old_break = interp.break_flag old_ret = interp.return_flag old_retval = interp.return_value interp.return_flag = False interp.return_value = None result = None block = method_node.get('block', []) if isinstance(block, list): for stmt in block: if interp.break_flag or interp.return_flag: break interp.execute_node(stmt) if interp.return_flag: result = interp.return_value break # Restaurar contexto interp.break_flag = old_break interp.return_flag = old_ret interp.return_value = old_retval interp._current_function = old_function interp._current_instance = old_instance interp.symbol_table.pop_scope(label=f"Método '{method_name}'") interp.symbol_table.pop_function_frame(frame_key) interp._log(f"[OOP] <- Método '{method_name}' resultado={result}") return result debug_mode_g = False # ============================================================================== # Sistema de carga de archivos fuente (.tss) # ============================================================================== class SourceFunctionWrapper: """ Envuelve una función declarada en un módulo fuente (.tss) para que exponga la misma interfaz .call(args, caller_interpreter) que usan los módulos nativos. """ def __init__(self, func_name: str, proxy: 'SourceModuleProxy'): self.func_name = func_name self.proxy = proxy def call(self, args, caller_interpreter): iso = self.proxy._isolated_interpreter ctx = FunctionContext(iso, self.func_name, args) return ctx.execute_function() class SourceModuleProxy: """ Envuelve un Interpreter aislado (que ejecutó un archivo .tss) y expone sus símbolos con la misma interfaz que usa el ModuleLoader nativo. """ def __init__(self, isolated_interpreter: 'Interpreter', module_name: str): self._isolated_interpreter = isolated_interpreter self._module_name = module_name # alias_name → original_name self._aliases: dict = {} # ── aliases ──────────────────────────────────────────────────────────── def apply_member_alias(self, original: str, alias: str): self._aliases[alias] = original def _real_name(self, name: str) -> str: return self._aliases.get(name, name) # ── acceso a funciones ───────────────────────────────────────────────── def get_function(self, func_name: str) -> SourceFunctionWrapper: real = self._real_name(func_name) iso = self._isolated_interpreter try: iso.symbol_table.get_function(real) except UndeclaredVariableError: raise ModuleError( f"La función '{real}' no existe en el módulo fuente '{self._module_name}'." ) return SourceFunctionWrapper(real, self) # ── acceso a variables / constantes ──────────────────────────────────── def get_export(self, var_name: str): real = self._real_name(var_name) iso = self._isolated_interpreter try: return iso.symbol_table.get_value(real) except UndeclaredVariableError: raise ModuleError( f"La variable '{real}' no existe en el módulo fuente '{self._module_name}'." ) def has_function(self, name: str) -> bool: try: self._isolated_interpreter.symbol_table.get_function(self._real_name(name)) return True except UndeclaredVariableError: return False def has_export(self, name: str) -> bool: try: self._isolated_interpreter.symbol_table.get_value(self._real_name(name)) return True except UndeclaredVariableError: return False def all_symbols(self) -> dict: """Devuelve todas las variables del scope global del módulo.""" iso = self._isolated_interpreter result = {} if iso.symbol_table.symbols: for name, sym in iso.symbol_table.symbols[0].items(): result[name] = sym.value return result def all_functions(self) -> list: return list(self._isolated_interpreter.symbol_table.function_symbols.keys()) class Interpreter: def __init__(self, debug_mode=False): self.debug_mode = debug_mode or debug_mode_g self.symbol_table = SymbolTable() self.symbol_table._debug_mode = self.debug_mode self.break_flag = False self.return_flag = False self.return_value = None self.switch_fall_through = False self._current_function = None self._current_instance: 'ClassInstance | None' = None # contexto OOP self._path_stack = [] self.module_loader = ModuleLoader(debug=self.debug_mode) self._source_modules: dict = {} self._base_dir: str = "" # ── OOP ────────────────────────────────────────────────────────────── self.object_table = ObjectSymbolTable() self.struct_table = StructSymbolTable() # ← tabla dedicada a structs self._main_function = None # ← función marcada como punto de entrada self._async_functions: set = set() # nombres de funciones async # ── Call stack para traceback enriquecido ───────────────────────────── self._call_stack: list = [] # lista de (file, function, line) self._source_lines: list = [] # líneas del archivo fuente activo self._current_line: int = None # línea del nodo en ejecución self._current_col: int = None # columna del nodo en ejecución self._source_file: str = "" # ruta al .tss real (para traceback) self._last_print_end: str = '\n' # end del último print (para restaurar \n) # ── Event Loop (para UI / async) ────────────────────────────────────── self._event_loop: 'asyncio.AbstractEventLoop | None' = None self._async_tasks: list = [] self._setup_source_module_hooks() self._tsc_config: dict = {} def _register_native_classes(self, module_name: str): """ Registra en self.object_table las clases nativas que el ModuleLoader haya cargado desde el módulo 'module_name'. """ ml = self.module_loader # Intenta obtener las clases del módulo. Se asume que ModuleLoader tiene # un método get_classes(module_name) que devuelve dict {nombre: ClassDefinition} if hasattr(ml, 'get_classes'): classes = ml.get_classes(module_name) for class_name, class_def in classes.items(): if not self.object_table.has_class(class_name): self.object_table.declare_class(class_name, class_def) self._log(f"[OOP] Clase nativa '{class_name}' registrada desde módulo '{module_name}'") else: # Fallback: si no existe get_classes, intenta acceder a un atributo interno común # (ajusta según la implementación real de ModuleLoader) if hasattr(ml, '_native_classes') and module_name in ml._native_classes: for class_name, class_def in ml._native_classes[module_name].items(): if not self.object_table.has_class(class_name): self.object_table.declare_class(class_name, class_def) self._log(f"[OOP] Clase nativa '{class_name}' registrada (fallback)") def _setup_source_module_hooks(self): """ Extiende el ModuleLoader nativo para que también consulte _source_modules de forma transparente, sin modificar module_loader.py. """ interp = self ml = self.module_loader _orig_is_loaded = ml.is_loaded _orig_get_function = ml.get_function _orig_get_export = ml.get_export_value def _is_loaded(name): return _orig_is_loaded(name) or name in interp._source_modules def _get_function(mod_name, func_name): if mod_name in interp._source_modules: return interp._source_modules[mod_name].get_function(func_name) return _orig_get_function(mod_name, func_name) def _get_export_value(mod_name, var_name): if mod_name in interp._source_modules: return interp._source_modules[mod_name].get_export(var_name) return _orig_get_export(mod_name, var_name) ml.is_loaded = _is_loaded ml.get_function = _get_function ml.get_export_value = _get_export_value def _log(self, message): """Función interna para imprimir mensajes solo si el modo debug está activo.""" if self.debug_mode: print(message) def interpret(self, ast, source_path: str = ""): import native_registry native_registry._current_interpreter = self # Actualizar directorio base para resolver rutas relativas en imports if source_path: self._base_dir = os.path.dirname(os.path.abspath(source_path)) base_dir = os.path.dirname(os.path.abspath(source_path)) self._path_stack.append(base_dir) # _source_file: el nombre para mostrar en el traceback viene # UNICAMENTE de --source (seteado en main() antes de interpret()). # Nunca se usa source_path (la ruta del .json) como nombre. # Cargar líneas del .tss real para mostrar en el traceback try: with open(self._source_file, 'r', encoding='utf-8') as _sf: self._source_lines = _sf.readlines() except Exception: # Fallback: si no se pudo abrir el .tss real, usar el .tss # reconstruido desde el AST (self._source_fallback_file), si # fue provisto. El nombre que aparece en el traceback sigue # siendo self._source_file (el .tss original) — esto solo # rellena el CONTENIDO de las líneas para poder mostrarlas. _fallback = getattr(self, '_source_fallback_file', None) if _fallback and os.path.isfile(_fallback): try: with open(_fallback, 'r', encoding='utf-8') as _sf: self._source_lines = _sf.readlines() except Exception: self._source_lines = [] else: self._source_lines = [] elif not self._base_dir: self._base_dir = os.getcwd() self._source_file = "" if "Program" not in ast: raise InterpreterError("El AST debe tener un nodo raíz 'Program'.", **self._lc()) program_nodes = ast["Program"] # ── entry_point-strict: comprobar ANTES de ejecutar nada ───────────── # Escanea el AST buscando una Function con target="main". # Si entry_point-strict=true y no existe, error inmediato. if self._tsc_config: _sm = self._tsc_config.get("strict-mode", {}) if _sm.get("entry_point-strict", False): _has_main = any( "Function" in _n and str(_n["Function"].get("target", "")).strip().lower() == "main" for _n in program_nodes ) if not _has_main: raise InterpreterError( "entry_point-strict activo: el programa debe declarar una " "función de entrada con '-> main' " "(ej: function start() -> main { ... }).", file=self._source_file, line=1, col=1, function=None ) i = 0 while i < len(program_nodes): node = program_nodes[i] node_type = list(node.keys())[0] # Lógica especial para el WhileLoop if node_type == "WhileLoop": # Asumimos que el siguiente nodo es el bloque del bucle if i + 1 < len(program_nodes) and "Block" in program_nodes[i + 1]: while_node_content = node["WhileLoop"] block_node_content = program_nodes[i + 1]["Block"] # Le pasamos el bloque directamente al manejador self.handle_WhileLoop(while_node_content, block_node_content) i += 2 # Saltamos el WhileLoop y su Bloque ya procesados continue else: # Si no hay bloque, lo ejecutamos sin cuerpo y avanzamos self.handle_WhileLoop(node["WhileLoop"], None) i += 1 elif node_type == "SwitchStatement": # Asumimos que el siguiente nodo es el bloque del bucle if i + 1 < len(program_nodes) and "block" in program_nodes[i + 1]: switch_node_content = node["SwitchStatement"] block_node_content = program_nodes[i + 1]["block"] # Inyectamos el bloque en el nodo para que el handler lo reciba switch_node_content["block"] = block_node_content self.handle_SwitchStatement(switch_node_content) i += 2 # Saltamos el SwitchStatement y su Bloque ya procesados continue else: # Un switch sin bloque no se puede ejecutar self.handle_SwitchStatement(node["SwitchStatement"]) i += 1 # Para todos los demás nodos, el comportamiento es el normal else: self.execute_node(node) i += 1 if source_path and self._path_stack: self._path_stack.pop() # ── Validación entry_point-strict ───────────────────────────────────── # ── Punto de entrada ->main: llamar tras registrar todo ─────────────── if self._main_function: self._log(f"[MAIN] Ejecutando punto de entrada: '{self._main_function}'") try: ctx = FunctionContext(self, self._main_function, {}) ctx.execute_function() except Exception as e: raise InterpreterError( f"[MAIN] Error en función de entrada '{self._main_function}': {e}", **self._lc()) def execute_node(self, node): if not node: return node_type = list(node.keys())[0] # Rastrear línea/col actual para excepciones sin acceso directo al nodo _inner = node.get(node_type, {}) if isinstance(_inner, dict): _lc_inner = _inner.get("linecolumn", {}) if _lc_inner: self._current_line = _lc_inner.get("line") self._current_col = _lc_inner.get("col") self._log(f"DEBUG: node_type = '{node_type}'") # Normalizar variantes camelCase de increment/decrement _REMAP = { 'postIncrementStatement': 'PostIncrementStatement', 'postDecrementStatement': 'PostDecrementStatement', 'preIncrementStatement': 'PreIncrementStatement', 'preDecrementStatement': 'PreDecrementStatement', } lookup = _REMAP.get(node_type, node_type) handler = getattr(self, f"handle_{lookup}", None) if handler is None: # AGREGADO: si no hay un handler con el nombre EXACTO del # node_type, intentar un match insensible a mayúsculas/ # minúsculas contra los handlers existentes antes de rendirse # en silencio con handle_unknown. Esto cubre variantes de # casing que el parser podría emitir para ciertos nodos (ej. # una variante de 'ConstantDeclaration' con distinto casing # para la forma tipada, 'const int x = ...'), que antes se # perdían en silencio sin declarar nada y sin ningún error # visible más allá de una advertencia — dejando variables # "fantasma" que después fallaban con NameError al usarlas. _lookup_lower = lookup.lower() for _attr in dir(self): if _attr.startswith('handle_') and _attr[len('handle_'):].lower() == _lookup_lower: handler = getattr(self, _attr) break if handler is None: handler = self.handle_unknown return handler(node[node_type]) # ========================================================================== # ¡CORRECCIÓN 1: Manejo de claves inconsistentes en el AST! # ========================================================================== def execute_block(self, block_node): if not isinstance(block_node, dict): return items = list(block_node.items()) i = 0 while i < len(items): if self.break_flag: return key, content = items[i] # Nodos que tienen su bloque como hermano en el AST y necesitan ser reparados NODES_TO_REPAIR = {"if_Condition", "forLoop", "whileLoop", "performWhileLoop"} if key in NODES_TO_REPAIR: # Mira hacia adelante para ver si el siguiente nodo es el bloque que le corresponde if i + 1 < len(items) and items[i + 1][0] == "block": # ¡Esta es la corrección! Inyecta el bloque en el nodo actual. content["block"] = items[i + 1][1] i += 1 # Incrementa el índice para saltar el nodo 'block' ya procesado # Ahora, ejecuta el nodo ya reparado node_type = key[0].upper() + key[1:] self.execute_node({node_type: content}) else: # Lógica para todos los demás nodos que no necesitan reparación EXECUTABLE_KEYS = { "variableDeclaration", "variableAsignement", "callExpression", "switchStatement", "function", "functionCall", "parameterAsignement", "postIncrementStatement", "postDecrementStatement", "preIncrementStatement", "preDecrementStatement", "tryCatch", "throw", "newObject", "classDeclaration", "interfaceDeclaration", "thisAccess", "thisCall", "thisAssignment", "superAccess", "superCall", "superAssignment", "superConstructorCall", "methodDeclaration", "attributeDeclaration", "attributeConstantDeclaration", "constantAttributeDeclaration", # AGREGADO: faltaba 'constantDeclaration' (la variante camelCase # de nivel superior para 'const x = ...;' / 'const int x = ...;', # paralela a 'variableDeclaration'). Sin esta clave, cualquier # sentencia const dentro de un bloque nunca llegaba a # execute_node — se saltaba en silencio, dejando la variable sin # declarar, y cualquier uso posterior (asignación o print) # fallaba con NameError como si nunca hubiera existido. "constantDeclaration" } if key in EXECUTABLE_KEYS: node_type = key[0].upper() + key[1:] if isinstance(content, list): for item in content: if self.break_flag: break self.execute_node({node_type: item}) else: self.execute_node({node_type: content}) i += 1 def handle_ArrayAccess(self, node): """Maneja acceso a arrays como d[i]""" array_name = node.get("array") index = node.get("index") try: array_value = self.symbol_table.get_value(array_name) index_value = self.symbol_table.get_value(index) # Convertir string de array a lista Python if isinstance(array_value, str) and array_value.startswith('[') and array_value.endswith(']'): try: array_list = json.loads(array_value.replace("'", '"')) if isinstance(array_list, list) and 0 <= index_value < len(array_list): return array_list[index_value] except: pass return f"" except UndeclaredVariableError: return f"" def _evaluate_array_access(self, array_expression): """ Evalúa accesos indexados simples y profundos sobre arrays, tuplas, dicts, campos de struct, y variables normales. Sintaxis: variable[i] → acceso simple variable[i:j] → acceso profundo nivel 2 variable[i:j:k] → acceso profundo nivel 3 p1.field[i] → acceso sobre campo de struct p1.field[i:j] → acceso profundo sobre campo de struct Índices permitidos: - Entero literal 0, 1, 42 - String literal "clave", 'clave' - Variable i, j, k (Para dicts se puede usar entero como posición O string como clave.) """ self._log(f" [INDEX] Evaluando acceso: {array_expression!r}") # ── 1. Separar nombre base y contenido del bracket ──────────────────── # Soporta: var[…], var.field[…], p1.dir.campo[…] bracket_match = re.match(r'^([\w.]+)\[(.+)\]$', array_expression, re.DOTALL) if not bracket_match: return f"" base_expr = bracket_match.group(1).strip() # "arr" / "p1.hobbies" raw_index_str = bracket_match.group(2).strip() # "0" / "0:1" / '"clave"' # ── 2. Resolver el valor base ───────────────────────────────────────── try: if '.' in base_expr: # Puede ser struct.field o módulo.var root = base_expr.split('.')[0] rest = '.'.join(base_expr.split('.')[1:]) try: root_val = self._resolve_root_value_for_read(root) if isinstance(root_val, StructInstance): current_structure = self._get_struct_field_by_path(root_val, rest) else: current_structure = self.resolve_expression(base_expr) except UndeclaredVariableError: current_structure = self.resolve_expression(base_expr) else: # AGREGADO: antes usaba symbol_table.get_value directo, que # nunca encuentra un PARÁMETRO de función (esos viven aparte # en function_param_values). Eso hacía que 'arreglo[j]' con # 'arreglo' parámetro nunca resolviera de verdad — ver # _resolve_root_value_for_read. current_structure = self._resolve_root_value_for_read(base_expr) except UndeclaredVariableError: raise NameError( msg("name.undefined", name=base_expr), **self._lc_here(base_expr) ) # JSON-string fallback if isinstance(current_structure, str): stripped = current_structure.strip() if (stripped.startswith('[') and stripped.endswith(']')) or \ (stripped.startswith('{') and stripped.endswith('}')): try: current_structure = json.loads(stripped.replace("'", '"')) except Exception: return f"" # ── 3. Dividir índices por ':' (respetando strings y brackets) ──────── indices_list = self._split_index_parts(raw_index_str) self._log(f" [INDEX] Índices: {indices_list}") # ── 4. Navegar nivel a nivel ────────────────────────────────────────── for level, raw_index in enumerate(indices_list): raw_index = raw_index.strip() # ── Resolver el índice (maneja strings, ints, vars, x[0], rangos) ── try: index_value = self._resolve_index_value(raw_index) except Exception as e: return f"" self._log(f" [INDEX] nivel {level}: índice={index_value!r} sobre {type(current_structure).__name__}") # Validar que la estructura es indexable if not isinstance(current_structure, (list, tuple, dict)): raise DeepNotCollectionError( msg("deep.not_collection", depth=level, got=type(current_structure).__name__), depth=level, got_type=type(current_structure).__name__, path=indices_list, **self._lc_here(array_expression) ) # Navegar try: if isinstance(current_structure, dict): # Dict: primero por clave directa, luego por posición if index_value in current_structure: current_structure = current_structure[index_value] elif isinstance(index_value, int): keys = list(current_structure.keys()) if 0 <= index_value < len(keys): current_structure = current_structure[keys[index_value]] else: raise DeepIndexOutOfRangeError( msg("deep.out_of_range", depth=level, index=index_value, length=len(keys)), depth=level, index=index_value, length=len(keys), path=indices_list, **self._lc_here(array_expression) ) else: raise DeepKeyError( msg("deep.key_not_found", depth=level, key=index_value), depth=level, key=index_value, path=indices_list, **self._lc_here(array_expression) ) elif isinstance(current_structure, (list, tuple)): # AGREGADO: soporte de slicing cuando el índice es un # RangeValue (ej. arr[0..2], arr[1..<4]). Antes esto caía # en la rama de índice entero normal, que intentaba # int(RangeValue(...)) y reventaba con # "int() argument must be a string, a bytes-like object # or a real number, not 'RangeValue'". if isinstance(index_value, RangeValue): length = len(current_structure) _range_indices = index_value.expand() if not _range_indices: current_structure = current_structure[0:0] else: _lo = min(_range_indices) _hi = max(_range_indices) # Validar que el rango completo cabe en la colección; # si el rango es mayor a los elementos disponibles, # error claro en vez de truncar en silencio. if _lo < 0 or _hi >= length: raise DeepIndexOutOfRangeError( msg("deep.out_of_range", depth=level, index=index_value.format_short(), length=length), depth=level, index=index_value.format_short(), length=length, path=indices_list, **self._lc_here(array_expression) ) _sliced = [current_structure[i] for i in _range_indices] current_structure = tuple(_sliced) if isinstance(current_structure, tuple) else _sliced elif not isinstance(index_value, int): try: index_value = int(index_value) except (TypeError, ValueError): raise DeepTypeMismatchError( msg("deep.type_mismatch", depth=level, got=type(index_value).__name__, expected="int"), depth=level, expected="int", got=type(index_value).__name__, path=indices_list, **self._lc_here(array_expression) ) length = len(current_structure) if -length <= index_value < length: current_structure = current_structure[index_value] else: raise DeepIndexOutOfRangeError( msg("deep.out_of_range", depth=level, index=index_value, length=length), depth=level, index=index_value, length=length, path=indices_list, **self._lc_here(array_expression) ) else: length = len(current_structure) if -length <= index_value < length: current_structure = current_structure[index_value] else: raise DeepIndexOutOfRangeError( msg("deep.out_of_range", depth=level, index=index_value, length=length), depth=level, index=index_value, length=length, path=indices_list, **self._lc_here(array_expression) ) except Exception as e: return f"" self._log(f" [INDEX] Resultado: {current_structure!r}") return current_structure def _apply_range_to_structure(self, rng: 'RangeValue', structure) -> object: """ Aplica un RangeValue como índice posicional sobre array, tupla o dict. Devuelve el mismo tipo de contenedor que la entrada: list → list (corchetes) tuple → tuple (paréntesis) dict → dict (llaves, preservando claves originales) """ if not isinstance(structure, (list, tuple, dict)): return (f"") expanded = rng.expand() if not expanded: # Devolver el contenedor vacío del mismo tipo return {} if isinstance(structure, dict) else ( tuple() if isinstance(structure, tuple) else []) for idx in expanded: if not isinstance(idx, int): return f"" length = len(structure) if isinstance(structure, dict): keys = list(structure.keys()) result = {} for idx in expanded: if idx < 0 or idx >= length: return (f"") k = keys[idx] result[k] = structure[k] return result # list / tuple — misma lógica, distinto contenedor items = [] for idx in expanded: if idx < 0 or idx >= length: return (f"") items.append(structure[idx]) return tuple(items) if isinstance(structure, tuple) else items def _resolve_index_value(self, raw_index: str): """ Resuelve un token de índice a su valor Python. Maneja todos los casos en orden: 1. String literal "clave" / 'clave' → str 2. Int literal -3, 0, 42 → int 3. Float literal 0.5 → float (poco común, pero válido para dict keys) 4. Rango literal 1..5 → RangeValue (para arr[1..3]) 5. Acceso indexado x[0], arr[1:2] → valor del sub-acceso 6. Variable simple x, i, j → symbol_table lookup 7. Expresión general → resolve_expression """ raw = raw_index.strip() # 1. String literal if (raw.startswith('"') and raw.endswith('"')) or \ (raw.startswith("'") and raw.endswith("'")): return raw[1:-1] # 2. Int literal (incluyendo negativo) if re.fullmatch(r'-?\d+', raw): return int(raw) # 3. Float literal if re.fullmatch(r'-?\d+\.\d+', raw): return float(raw) # 4. Rango literal if '..' in raw: rng = self._try_parse_range(raw) if rng is not None: return rng # 5. Acceso indexado sobre otra variable: x[0], arr[1:2], p1.tags[0] if re.search(r'\[', raw): return self._evaluate_array_access(raw) # 6. Variable simple → parámetro de función o symbol_table if re.fullmatch(r'[A-Za-z_]\w*', raw): try: return self._resolve_root_value_for_read(raw) except UndeclaredVariableError: raise InterpreterError( f"Variable índice '{raw}' no declarada", **self._lc()) # 7. Expresión general try: return self.resolve_expression(raw) except Exception as e: raise InterpreterError( f"No se pudo resolver el índice '{raw}': {e}", **self._lc()) """ Aplica acceso indexado (simple o profundo) directamente sobre un valor Python, sin buscarlo en la tabla de símbolos. """ indices_list = self._split_index_parts(raw_index_str) current = value for level, raw_index in enumerate(indices_list): raw_index = raw_index.strip() try: index_value = self._resolve_index_value(raw_index) except InterpreterError as e: return f"" # Si el índice es un RangeValue → retornar slice if isinstance(index_value, RangeValue): result = self._apply_range_to_structure(index_value, current) if isinstance(result, str) and result.startswith('") try: if isinstance(current, dict): if index_value in current: current = current[index_value] elif isinstance(index_value, int): keys = list(current.keys()) if 0 <= index_value < len(keys): current = current[keys[index_value]] else: return f"" else: return f"" else: if not isinstance(index_value, int): try: index_value = int(index_value) except (TypeError, ValueError): return f"" length = len(current) if -length <= index_value < length: current = current[index_value] else: return f"" except Exception as e: return f"" return current def _split_index_parts(self, raw: str) -> list: """ Divide 'a:b:c' por ':' respetando strings y brackets anidados. '0:"clave":2' → ['0', '"clave"', '2'] '"key":0' → ['"key"', '0'] """ parts, buf, depth, in_str, str_char = [], [], 0, False, '"' i = 0 while i < len(raw): ch = raw[i] if in_str: buf.append(ch) if ch == str_char and (i == 0 or raw[i-1] != '\\'): in_str = False elif ch in ('"', "'"): in_str = True; str_char = ch; buf.append(ch) elif ch in ('(', '[', '{'): depth += 1; buf.append(ch) elif ch in (')', ']', '}'): depth -= 1; buf.append(ch) elif ch == ':' and depth == 0: parts.append(''.join(buf).strip()); buf = [] else: buf.append(ch) i += 1 if buf: parts.append(''.join(buf).strip()) return [p for p in parts if p] def _resolve_embedded_function_calls(self, expression_str): """ Resuelve llamadas embebidas dentro de una expresión. Primero resuelve mod.func() (módulos), luego funciones declaradas. Guard de re-entrada: si ya estamos dentro de esta función para la misma expresión, devolvemos la expresión tal cual para cortar la recursión. """ # Guard de re-entrada simple con contador de profundidad if not hasattr(self, '_embedded_depth'): self._embedded_depth = 0 if self._embedded_depth > 20: return expression_str self._embedded_depth += 1 try: return self._resolve_embedded_function_calls_impl(expression_str) finally: self._embedded_depth -= 1 def _resolve_embedded_function_calls_impl(self, expression_str): # Patrón módulo: mod.func(args) — se resuelve PRIMERO mod_pattern = r'([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\(([^()]*)\)' def replace_mod_call(match): mod_name = match.group(1) func_name = match.group(2) raw_args = match.group(3) if not self.module_loader.is_loaded(mod_name): return match.group(0) args = self._parse_call_parameters({"value": raw_args}) if raw_args.strip() else [] try: fn = self.module_loader.get_function(mod_name, func_name) result = fn.call(args, self) return str(result) except TesseractError: raise except Exception: return match.group(0) prev = None while prev != expression_str: prev = expression_str expression_str = re.sub(mod_pattern, replace_mod_call, expression_str) """Detecta llamadas a función dentro de una expresión y las reemplaza por su valor.""" # Patrón: nombre_funcion(argumentos) pattern = r'([a-zA-Z_][a-zA-Z0-9_]*)\(([^()]*)\)' def replace_call(match): full_match = match.group(0) func_name = match.group(1) # Ignorar palabras que no son funciones del lenguaje if func_name in ['if', 'while', 'for', 'perform', 'return', 'print', 'read']: return full_match # Verificar que existe como función declarada, O que es una función # nativa del núcleo (__tesseract_config, __tesseract_capabilities, ...) # que no vive en symbol_table.function_symbols. if func_name not in NATIVE_CORE_FUNCTIONS: try: self.symbol_table.get_function(func_name) except UndeclaredVariableError: return full_match # Ejecutar la función y retornar su valor try: result = self._execute_function_call_from_string(full_match) except TesseractError: raise except Exception: return full_match return str(result) # Resolver de adentro hacia afuera (puede haber recursión anidada) prev = None while prev != expression_str: prev = expression_str expression_str = re.sub(pattern, replace_call, expression_str) return expression_str def _resolve_embedded_array_access(self, expression_str): """ Resuelve accesos indexados PROFUNDOS (var["a":"b"], var[i:j:k], p1.campo[i:j]) embebidos dentro de una expresión mayor (ej. una comparación: config["strict-mode":"typing"] == "free") ANTES de sustituir variables y pasar por eval(). Por qué hace falta: 'evaluate_expression' sustituye 'config' por su valor literal y deja el '[...]' colgando para que lo interprete el eval() de Python — pero la sintaxis de acceso profundo de Tesseract usa ':' dentro de un solo corchete (var[i:j:k]) para encadenar niveles, y esa es EXACTAMENTE la sintaxis de slice de Python (a[i:j:k]). El resultado es que eval() arma un slice(...) en vez de navegar el diccionario/lista, y como los dicts no aceptan slice como clave, revienta con 'operando slice(...) inválido'. Solo se intercepta cuando hay más de un nivel de índice (':' de tope, fuera de comillas/corchetes anidados) — el acceso simple var[0] / var["clave"] se deja intacto para el camino normal (que ya funciona bien vía sustitución + eval, sin ambigüedad de slice). """ if '[' not in expression_str or ']' not in expression_str: return expression_str # base[contenido] — base admite cadena de puntos (p1.campo[i:j]) pattern = r'([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\[([^\[\]]+)\]' def replace_access(match): full_match = match.group(0) raw_index = match.group(2) # ¿Cuántos niveles de índice hay? (respeta comillas/corchetes anidados) try: parts = self._split_index_parts(raw_index) except Exception: return full_match if len(parts) < 2: # Acceso simple → dejar para el camino normal (eval directo) return full_match try: result = self._evaluate_array_access(full_match) except TesseractError: raise except Exception: return full_match if isinstance(result, str) and result.startswith(' and > not > comparaciones encadenadas (==,!=,<,<=,>,>=, # in, not in) > +,- > *,/,//,% > unario +/- > ** > átomo # # y devuelve EXACTAMENTE el mismo resultado que producía eval() (incluidos # los casos raros: cortocircuito real de and/or/comparaciones encadenadas, # promoción int/float, igualdad de listas/dicts/tuplas, etc.). # # Los errores (división por cero, tipos incompatibles, nombres sin # resolver) se señalizan con las excepciones internas _MEDivZero/ # _METypeError/_MEName/_MESyntax de más arriba, y se traducen a los # mismos errores de Tesseract que ya se producían antes en el try/except # que envolvía el eval() (ver evaluate_expression más abajo) — incluido # el comportamiento previo (no "arreglado") de que NameError/TypeError # de Python caían siempre al fallback genérico por el shadowing de esos # nombres con las clases de tesseract_errors. # ══════════════════════════════════════════════════════════════════════ _ME_NUMBER_RE = re.compile( r'\d+\.\d+(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?|\d+(?:[eE][+-]?\d+)?' ) _ME_OR_RE = re.compile(r'\|\||\bor\b') _ME_AND_RE = re.compile(r'&&|\band\b') _ME_NOT_RE = re.compile(r'^\s*(?:!(?!=)|\bnot\b)\s*') _ME_CMP_RE = re.compile(r'==|!=|<=|>=|!in\b|\bin\b|<|>') _ME_MUL_RE = re.compile(r'//|(? 0: out[i] = 'x' i += 1 continue m = self._ME_NUMBER_RE.match(expr, i) if m and m.end() > i: for j in range(i, m.end()): out[j] = 'N' i = m.end() continue i += 1 return ''.join(out) def _me_strip_wrapping_parens(self, expr): """ Si TODA la expresión está envuelta en un único par de paréntesis de agrupación redundante (no una tupla — es decir, sin coma de nivel superior dentro), los quita. Repite mientras aplique: "((x))" -> "x". """ s = expr.strip() while s.startswith('(') and s.endswith(')') and len(s) >= 2: masked = self._me_mask(s) depth = 0 close_pos = None for i, ch in enumerate(masked): if ch == '(': depth += 1 elif ch == ')': depth -= 1 if depth == 0: close_pos = i break if close_pos != len(s) - 1: break inner = s[1:-1].strip() if not inner: break # "()" — tupla vacía, se maneja como átomo if ',' in self._me_mask(inner): break # tiene coma de nivel superior -> es una tupla, no agrupación s = inner return s def _me_split_top_level_commas(self, expr): """Divide `expr` por sus comas de NIVEL SUPERIOR (para listas/tuplas/dicts).""" masked = self._me_mask(expr) parts = [] prev = 0 for i, ch in enumerate(masked): if ch == ',': parts.append(expr[prev:i]) prev = i + 1 parts.append(expr[prev:]) return [p.strip() for p in parts] def _me_split_words(self, expr, compiled_re): """ Divide `expr` en los operandos separados por las ocurrencias de NIVEL SUPERIOR de `compiled_re` (usado para or/and). Devuelve la lista de operandos (texto crudo) o None si no hay ninguna ocurrencia de nivel superior. """ masked = self._me_mask(expr) matches = list(compiled_re.finditer(masked)) if not matches: return None operands = [] prev_end = 0 for m in matches: operands.append(expr[prev_end:m.start()]) prev_end = m.end() operands.append(expr[prev_end:]) return operands def _me_split_comparison_chain(self, expr): """ Encuentra todas las comparaciones de NIVEL SUPERIOR (==, !=, <=, >=, <, >, in, not in) y devuelve (lista_de_operandos, lista_de_ops), o None si no hay ninguna. """ masked = self._me_mask(expr) matches = [] for m in self._ME_CMP_RE.finditer(masked): # AGREGADO: el '<' de un rango semi-abierto ('1..<10') NO es un # operador de comparación — es parte del propio literal de # rango (ver _me_atom). Sin este filtro, '10 in 1..<10' se # partía mal en 'in' Y '<' por separado, rompiendo el rango. if m.group(0) == '<' and m.start() >= 2 and masked[m.start() - 2:m.start()] == '..': continue matches.append(m) if not matches: return None operands = [] ops = [] prev_end = 0 for m in matches: operands.append(expr[prev_end:m.start()]) op_text = m.group(0) op_norm = 'not in' if op_text == '!in' else op_text ops.append(op_norm) prev_end = m.end() operands.append(expr[prev_end:]) return operands, ops def _me_is_binary_pm(self, expr, pos): """ True si el '+'/'-' en `pos` es un operador BINARIO (no un signo unario). Es unario si no hay nada antes (inicio de expresión) o si el carácter no-espacio anterior es otro operador o un paréntesis de apertura. """ j = pos - 1 while j >= 0 and expr[j].isspace(): j -= 1 if j < 0: return False return expr[j] not in '+-*/%(' def _me_split_additive(self, expr): """Divide `expr` por sus +/- BINARIOS de nivel superior.""" masked = self._me_mask(expr) positions = [] for m in re.finditer(r'[+\-]', masked): pos = m.start() if self._me_is_binary_pm(expr, pos): positions.append(pos) if not positions: return None operands = [] ops = [] prev_end = 0 for pos in positions: operands.append(expr[prev_end:pos]) ops.append(expr[pos]) prev_end = pos + 1 operands.append(expr[prev_end:]) return operands, ops def _me_split_multiplicative(self, expr): """Divide `expr` por sus *, /, //, % de nivel superior.""" masked = self._me_mask(expr) matches = list(self._ME_MUL_RE.finditer(masked)) if not matches: return None operands = [] ops = [] prev_end = 0 for m in matches: operands.append(expr[prev_end:m.start()]) ops.append(m.group(0)) prev_end = m.end() operands.append(expr[prev_end:]) return operands, ops def _me_split_power_first(self, expr): """Encuentra la PRIMERA '**' de nivel superior (right-assoc).""" masked = self._me_mask(expr) m = re.search(r'\*\*', masked) if not m: return None return expr[:m.start()], expr[m.end():] def _me_truthy(self, value): return bool(value) def _me_compare(self, op, left, right): try: if op == '==': return left == right if op == '!=': return left != right if op == '<': return left < right if op == '<=': return left <= right if op == '>': return left > right if op == '>=': return left >= right if op == 'in': return left in right if op == 'not in': return left not in right except TypeError as e: raise _METypeError(str(e)) raise _MESyntax(f"operador de comparación desconocido: {op}") def _me_arith(self, op, left, right): try: if op == '+': return left + right if op == '-': return left - right if op == '*': return left * right if op == '/': return left / right if op == '//': return left // right if op == '%': return left % right if op == '**': return left ** right except ZeroDivisionError: raise _MEDivZero() except TypeError as e: raise _METypeError(str(e)) raise _MESyntax(f"operador aritmético desconocido: {op}") def _master_eval(self, expr): """Punto de entrada del evaluador maestro (precedencia completa).""" expr = expr.strip() if expr == '': raise _MESyntax(expr) expr = self._me_strip_wrapping_parens(expr) # 1. or parts = self._me_split_words(expr, self._ME_OR_RE) if parts is not None: result = False for part in parts: result = self._master_eval(part) if self._me_truthy(result): return result return result # 2. and parts = self._me_split_words(expr, self._ME_AND_RE) if parts is not None: result = True for part in parts: result = self._master_eval(part) if not self._me_truthy(result): return result return result # 3. not / ! m = self._ME_NOT_RE.match(expr) if m and m.end() > 0 and m.group(0).strip() != '': return not self._me_truthy(self._master_eval(expr[m.end():])) # 4. comparaciones encadenadas (==, !=, <=, >=, <, >, in, not in) cmp = self._me_split_comparison_chain(expr) if cmp is not None: operands, ops = cmp left_val = self._master_eval(operands[0]) for i, op in enumerate(ops): right_val = self._master_eval(operands[i + 1]) if not self._me_compare(op, left_val, right_val): return False left_val = right_val return True # 5. aritmética (aditivo -> multiplicativo -> unario -> potencia -> átomo) return self._me_additive(expr) def _me_additive(self, expr): expr = expr.strip() add = self._me_split_additive(expr) if add is not None: operands, ops = add result = self._me_multiplicative(operands[0]) for i, op in enumerate(ops): right = self._me_multiplicative(operands[i + 1]) result = self._me_arith(op, result, right) return result return self._me_multiplicative(expr) def _me_multiplicative(self, expr): expr = expr.strip() mul = self._me_split_multiplicative(expr) if mul is not None: operands, ops = mul result = self._me_unary(operands[0]) for i, op in enumerate(ops): right = self._me_unary(operands[i + 1]) result = self._me_arith(op, result, right) return result return self._me_unary(expr) def _me_unary(self, expr): s = expr.strip() if s.startswith('+') and not self._ME_NUMBER_RE.match(s): return self._me_unary(s[1:]) if s.startswith('+'): # "+5" con el signo pegado al número — tratar como unario igual return self._me_unary(s[1:]) if s.startswith('-'): val = self._me_unary(s[1:]) try: return -val except TypeError as e: raise _METypeError(str(e)) return self._me_power(s) def _me_power(self, expr): s = expr.strip() pw = self._me_split_power_first(s) if pw is not None: base_text, exp_text = pw base = self._me_postfix(base_text) exponent = self._me_unary(exp_text) return self._me_arith('**', base, exponent) return self._me_postfix(s) def _me_find_group_end(self, expr, start): """ Si expr[start] es una comilla o un delimitador de apertura ('(', '[', '{'), devuelve el índice justo DESPUÉS del delimitador de cierre correspondiente (respetando anidamiento y comillas internas). Si expr[start] no es ninguno de esos, devuelve None. """ if start >= len(expr): return None ch0 = expr[start] n = len(expr) if ch0 in ('"', "'"): i = start + 1 while i < n: if expr[i] == ch0 and expr[i - 1] != '\\': return i + 1 i += 1 return None if ch0 in '([{': pairs = {'(': ')', '[': ']', '{': '}'} closer = pairs[ch0] depth = 0 i = start in_str = False str_ch = '"' while i < n: c = expr[i] if in_str: if c == str_ch and expr[i - 1] != '\\': in_str = False i += 1 continue if c in ('"', "'"): in_str = True str_ch = c i += 1 continue if c == ch0: depth += 1 elif c == closer: depth -= 1 if depth == 0: return i + 1 i += 1 return None return None def _me_split_postfix(self, expr): """ Separa `expr` en (texto_del_átomo_base, [textos_de_índice...]), donde cada índice corresponde a un '[...]' encadenado inmediatamente después del átomo base — ej. "[1,2,3][0]" o "arr[i][j]" (dos accesos simples encadenados, resultado de sustituir variables por su valor literal antes de llegar aquí). Si no hay ningún '[...]' colgando después del átomo base, devuelve (expr, []) sin cambios. """ s = expr.strip() if not s: return s, [] end = self._me_find_group_end(s, 0) if end is None: # Átomo "plano" (número, True/False/None, identificador) sin # comilla/bracket inicial: el subscript, si lo hay, empieza en # el primer '[' de nivel superior. m = re.match(r'[^\[]*', s) end = m.end() if m else len(s) base_text = s[:end] rest = s[end:] subscripts = [] while True: rest_stripped = rest.lstrip() if not rest_stripped.startswith('['): break bracket_end = self._me_find_group_end(rest_stripped, 0) if bracket_end is None: break idx_text = rest_stripped[1:bracket_end - 1] subscripts.append(idx_text) rest = rest_stripped[bracket_end:] if rest.strip(): # Sobró texto que no es ni base ni subscript válido -> no es # realmente un postfix subscript; devolver todo como base tal # cual para que el resto de _me_atom decida (o falle con el # mismo tipo de error que antes). return s, [] return base_text, subscripts def _me_postfix(self, expr): """ AGREGADO: soporte de indexado simple encadenado — base[índice], base[i][j], etc. — sobre listas/tuplas/strings/dicts literales (ya sustituidos). Esto es lo que antes resolvía el eval() real de Python de forma nativa (ej. "[3,1,4][0] > [3,1,4][1]"); el acceso PROFUNDO (con ':' encadenando niveles, tipo arr[i:j]) ya se resuelve aparte, ANTES, por _resolve_embedded_array_access — esto sólo cubre el índice simple que ese paso deja intacto a propósito. """ base_text, subscripts = self._me_split_postfix(expr) value = self._me_atom(base_text) for idx_text in subscripts: index_val = self._master_eval(idx_text) try: value = value[index_val] except (TypeError, IndexError, KeyError) as e: raise _METypeError(str(e)) return value def _me_atom(self, expr): s = expr.strip() if s == '': raise _MESyntax(expr) # ── Paréntesis de agrupación (no tupla) ────────────────────────── # BUG evitado: "(2+3)" NO es un átomo — es una expresión completa # que hay que volver a evaluar de punta a punta (puede incluso # tener and/or/comparaciones dentro, ej: "(x > 2 and y < 5) * 3"). # _me_strip_wrapping_parens sólo quita los paréntesis cuando son # agrupación redundante (no cuando es una tupla real con coma de # nivel superior); si cambió algo, hay que reevaluar el interior # completo con _master_eval, no intentar matchear un átomo simple. if s.startswith('(') and s.endswith(')') and len(s) >= 2: unwrapped = self._me_strip_wrapping_parens(s) if unwrapped != s: return self._master_eval(unwrapped) # Si no cambió: es una tupla (coma de nivel superior) o "()" vacía # — se maneja explícitamente más abajo. # Número (int o float, incluida notación científica) m = re.fullmatch(self._ME_NUMBER_RE, s) if m: text = m.group(0) if '.' in text or 'e' in text or 'E' in text: return float(text) return int(text) # AGREGADO: Rango literal — '1..10', '1..<10', '"a".."z"', 'null..null'. # Antes _me_atom no sabía qué hacer con un rango "suelto" (fuera de # un índice arr[i:j]) y terminaba en el _MESyntax de más abajo — por # eso 'if(1..10 in 5)' reventaba con "invalid syntax". Reutiliza # _try_parse_range (misma lógica que ya usan arr[1..3] y los for), # así que sigue soportando 'in'/'not in' contra el resultado gracias # a RangeValue.__contains__ (ver arriba), y también arrays/tuplas/ # dicts (que ya son list/tuple/dict de Python, soportan 'in' nativo). if '..' in s: rng = self._try_parse_range(s) if rng is not None: return rng # String literal if (s.startswith('"') and s.endswith('"') and len(s) >= 2) or \ (s.startswith("'") and s.endswith("'") and len(s) >= 2): inner = s[1:-1] return (inner.replace('\\n', '\n').replace('\\t', '\t') .replace('\\r', '\r').replace('\\"', '"') .replace("\\'", "'").replace('\\\\', '\\')) # True / False / None (resultan de str() de valores Python ya sustituidos) if s == 'True': return True if s == 'False': return False if s == 'None': return None # Lista [ ... ] if s.startswith('[') and s.endswith(']'): inner = s[1:-1].strip() if not inner: return [] items = self._me_split_top_level_commas(inner) if items and items[-1].strip() == '': items = items[:-1] # coma final: "[1,2,]" == "[1,2]" return [self._master_eval(it) for it in items] # Tupla ( ..., ... ) o "()" vacía — si llega aquí con paréntesis es # porque el bloque de arriba determinó que NO es agrupación # redundante (tiene coma de nivel superior, o está vacía). if s.startswith('(') and s.endswith(')'): inner = s[1:-1].strip() if not inner: return () items = self._me_split_top_level_commas(inner) # Coma final: "(1,)" es una tupla de UN elemento, no un elemento # vacío extra — se quita el último ítem si quedó vacío, pero # SIGUE siendo una tupla (no se colapsa al valor simple). if items and items[-1].strip() == '': items = items[:-1] return tuple(self._master_eval(it) for it in items) # Dict { ... } if s.startswith('{') and s.endswith('}'): inner = s[1:-1].strip() if not inner: return {} pairs = self._me_split_top_level_commas(inner) if pairs and pairs[-1].strip() == '': pairs = pairs[:-1] # coma final: '{"a":1,}' == '{"a":1}' result = {} for pt in pairs: masked_pt = self._me_mask(pt) colon_pos = masked_pt.find(':') if colon_pos == -1: raise _MESyntax(pt) key = self._master_eval(pt[:colon_pos]) val = self._master_eval(pt[colon_pos + 1:]) result[key] = val return result # Identificador que sobrevivió sin resolver if re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', s): raise _MEName(s) raise _MESyntax(s) def evaluate_expression(self, expression_str): """ Evalúa expresiones booleanas y lógicas. Reemplaza variables y parámetros por sus valores antes de evaluar. """ # ── Regla universal: si está entre comillas es string literal ───── _es = expression_str.strip() if isinstance(expression_str, str) else '' def _is_single_quoted_literal(s, qch): # AGREGADO: antes, cualquier texto que empezara y terminara con # comillas se trataba como UN string literal — incluso si en # realidad era una expresión compuesta con VARIOS segmentos entre # comillas, ej. '"m" in "a".."z"' (rango de strings + in). Eso # devolvía basura (el texto completo tal cual, con comillas y # todo) en vez de evaluar la expresión. Ahora sólo cuenta como # literal simple si NO hay otra comilla del mismo tipo (sin # escapar) en el medio. if not (s.startswith(qch) and s.endswith(qch) and len(s) >= 2): return False inner = s[1:-1] i = 0 while i < len(inner): if inner[i] == '\\': i += 2 continue if inner[i] == qch: return False i += 1 return True if _is_single_quoted_literal(_es, '"') or _is_single_quoted_literal(_es, "'"): return self._interpolate_hash_string(_es[1:-1]) # ══════════════════════════════════════════════════════════════════ # AGREGADO: atajo de verdad para identificador simple — if(x), while(x), # for(...; x; ...), perform-while, elseif(x), etc. Todos estos campos # de condición delegan (directo o vía _evaluate_condition) en esta # misma función, así que basta con resolverlo UNA vez aquí. # # - x contiene el resultado de una comparación (ej. var x = 2==2; # o x = 5 !in arr;) → x YA es un bool real (True/False), se usa # tal cual: si es true la condición pasa, si es false no. # - x es int/float → 0 = false, cualquier otro valor = true (estilo C). # - cualquier otro valor (string, lista, dict, null...) → verdad # estándar (None/""/[]/{} = false, el resto = true) — el mismo # comportamiento de siempre para if(x) con x = 0, "abc", etc. # # Sólo aplica cuando TODA la condición es un identificador simple — # no toca comparaciones, in/!in, aritmética ni nada compuesto, que # siguen su camino normal sin cambios. # ══════════════════════════════════════════════════════════════════ if re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', _es) and \ _es not in ('true', 'false', 'null', 'and', 'or', 'not'): _bare_value = _UNRESOLVED if hasattr(self, '_current_function') and self._current_function: _fps = self.symbol_table.get_function_params(self._current_function) if _es in _fps: try: _bare_value = self.symbol_table.get_function_param_value( self._current_function, _es) except UndeclaredVariableError: pass if _bare_value is _UNRESOLVED: try: _bare_value = self.symbol_table.get_value(_es) except UndeclaredVariableError: _bare_value = _UNRESOLVED if _bare_value is not _UNRESOLVED: self._log(f" Atajo de verdad para identificador simple '{_es}': {_bare_value!r}") return bool(_bare_value) # ── NUEVO: resolver mod.func() ANTES de reemplazar variables ────── mod_pattern = r'([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\(([^()]*)\)' def replace_mod(match): mod_name = match.group(1) func_name = match.group(2) raw_args = match.group(3) if not self.module_loader.is_loaded(mod_name): return match.group(0) args = self._parse_call_parameters({"value": raw_args}) if raw_args.strip() else [] try: fn = self.module_loader.get_function(mod_name, func_name) return str(fn.call(args, self)) except Exception: return match.group(0) prev = None while prev != expression_str: prev = expression_str expression_str = re.sub(mod_pattern, replace_mod, expression_str) # ===== INICIALIZAR function_params para evitar NameError ===== expression_str = self._resolve_embedded_function_calls(expression_str) # ===== Resolver accesos indexados profundos (var[i:j:k]) ANTES de ===== # ===== sustituir variables, para que no colisionen con slice() de eval ===== expression_str = self._resolve_embedded_array_access(expression_str) function_params = {} # ===== BUSCAR EN PARÁMETROS DE FUNCIÓN SI ESTAMOS EN UNA ===== if hasattr(self, '_current_function') and self._current_function: function_params = self.symbol_table.get_function_params(self._current_function) for param_name in function_params.keys(): # Buscar el parámetro como palabra completa en la expresión if re.search(r'\b' + param_name + r'\b', expression_str): try: param_value = self.symbol_table.get_function_param_value( self._current_function, param_name ) # Reemplazar solo si el parámetro existe como palabra completa _pv = str(param_value) expression_str = re.sub(r'\b' + re.escape(param_name) + r'\b', lambda m, s=_pv: s, expression_str) self._log(f" Reemplazado parámetro '{param_name}': {param_value}") except UndeclaredVariableError: pass # Parámetro sin valor # El regex ahora captura también el grupo de argumentos opcionales (\(…\)) # para que m.group(0) incluya la llamada completa: arr.contains("Dog") # y no solo arr.contains — lo que dejaba ("Dog") huérfano en la expresión. inst_dot = re.compile( r'\b([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)(\((?:[^()]*|\([^()]*\))*\))?' ) def _replace_inst_field(m): obj_n = m.group(1) field_n = m.group(2) args_part = m.group(3) or '' # '("Dog")' o '' full_expr = m.group(0) # 'arr.contains("Dog")' o 'arr.length' try: obj = self.symbol_table.get_value(obj_n) # ── StructInstance field access ────────────────────────────────── if isinstance(obj, StructInstance): rest_path = field_n + args_part try: val = self._get_struct_field_by_path(obj, rest_path) if isinstance(val, StructInstance): return val.format_print(f"{obj_n}.{field_n}") return json.dumps(val) if isinstance(val, str) else str(val) except Exception: pass if isinstance(obj, ClassInstance): if not args_part: # acceso a atributo puro try: val = obj.get_attribute(field_n) return json.dumps(val) if isinstance(val, str) else str(val) except Exception: pass # Llamada a método OOP → dejar que _resolve_dot_chain lo maneje # Resolver métodos de tipo core (arr.contains("Dog"), arr.length, etc.) resolved = self._resolve_dot_chain(full_expr) if resolved is not _UNRESOLVED: return json.dumps(resolved) if isinstance(resolved, str) else str(resolved) except TesseractError: raise except (UndeclaredVariableError, Exception): pass return m.group(0) expression_str = inst_dot.sub(_replace_inst_field, expression_str) # ===== BUSCAR VARIABLES NORMALES (EXCEPTO PARÁMETROS YA REEMPLAZADOS) ===== # Solo buscar en tokens fuera de comillas para no confundir "a" con variable a _stripped_expr = re.sub(r'"' + r'[^"]*"' + r'|\'[^\']*\'', '\"\"', expression_str) variable_names = re.findall(r'[a-zA-Z_][a-zA-Z0-9_]*', _stripped_expr) value_str = "" for var in set(variable_names): # AGREGADO: 'null'/'NULL' ya no se buscan como variable (van directo # al bloque de abajo que las traduce a None de Python para el eval). # 'None' YA NO es palabra reservada — 'None' no existe en el # lenguaje, sólo 'null'. Si el usuario escribe 'None' a secas, debe # comportarse como CUALQUIER identificador no declarado (error), y # no colarse silenciosamente como null — cosa que pasaría si se # dejara intacto para el eval() de Python, ya que None SÍ es un # literal válido de Python aunque nunca se haya declarado como # variable en symbol_table. if var in ('null', 'NULL'): continue if var not in ['true', 'false', 'and', 'or', 'not'] and var not in function_params.keys(): try: value = self.symbol_table.get_value(var) if isinstance(value, (ClassInstance, ModuleInstance)): continue if isinstance(value, str): value_str = json.dumps(value) else: value_str = str(value) expression_str = re.sub(r'\b' + re.escape(var) + r'\b', lambda m, s=value_str: s, expression_str) self._log(f" Reemplazada variable '{var}': {value}") except UndeclaredVariableError: if var == 'None': # 'None' no existe como palabra reservada: se trata # exactamente igual que cualquier otra variable no # declarada, en vez de dejarla pasar para que Python la # interprete como su propio literal None. raise NameError( msg("name.undefined", name=var), **self._lc() ) pass # dejar para que eval() lo detecte como NameError # ===== 'null' / 'NULL' (única forma reservada de valor nulo del # lenguaje) → None de Python, para que el eval() interno lo entienda. # Esto es igual de "plomería interna" que el && -> and de la línea de # abajo: no expone 'None' como sintaxis válida del usuario, sólo # traduce la palabra reservada real ('null') al literal que Python # necesita para comparar/operar. expression_str = re.sub(r'\b(?:null|NULL)\b', 'None', expression_str) # ===== REEMPLAZAR OPERADORES LÓGICOS DESPUÉS DE LAS VARIABLES ===== expression_str = expression_str.replace("&&", " and ").replace("||", " or ") # ===== INTERCEPTAR LITERAL.METODO ANTES DEL EVAL ===== # Si la expresión es un literal (bool, número, string, array, tupla, dict) # seguido de .algo, el eval de Python lanzaría AttributeError porque # True.type, (1).type, etc. no existen en Python. # Se intercepta generalmente: si tiene punto y la parte izquierda es # un literal reconocible, se manda a resolve_expression que ya lo maneja. _es_strip = expression_str.strip() _interceptar = False if '.' in _es_strip: # bool literal: true.X / false.X / True.X / False.X if re.match(r'^(true|false|True|False)\.', _es_strip): _interceptar = True # null literal: null.X / NULL.X / None.X elif re.match(r'^(null|NULL|None)\.', _es_strip): _interceptar = True # numero.X (ej: 2.type, -3.length) — excluir decimales como 3.14 elif re.match(r'^-?\d+\.[A-Za-z_]', _es_strip): _interceptar = True # string literal: "...".X o '...'.X # REPARADO: el regex anterior (r'^(["\']).*\1\.') usaba '.*' greedy, # que encontraba la ÚLTIMA comilla-seguida-de-punto en la cadena en # vez de la PRIMERA — confundiendo expresiones compuestas como # '"m" in "a".."z"' (rango de strings) con un literal.method simple, # y devolviendo basura en vez de evaluar el 'in'. Ahora se escanea # manualmente hasta la PRIMERA comilla de cierre real (respetando # escapes) y sólo se intercepta si el punto viene INMEDIATAMENTE # después de esa comilla de cierre. elif _es_strip[0] in ('"', "'"): _qch = _es_strip[0] _j = 1 _closed = False while _j < len(_es_strip): if _es_strip[_j] == '\\': _j += 2 continue if _es_strip[_j] == _qch: _closed = True break _j += 1 if _closed and _j + 1 < len(_es_strip) and _es_strip[_j + 1] == '.': _interceptar = True # array: [...].X elif re.match(r'^\[.*\]\.', _es_strip, re.DOTALL): _interceptar = True # tupla: (...,...).X (con al menos una coma al nivel raiz) elif re.match(r'^\(.*,.*\)\.', _es_strip, re.DOTALL): _interceptar = True # dict: {...}.X elif re.match(r'^\{.*\}\.', _es_strip, re.DOTALL): _interceptar = True if _interceptar: _norm = _es_strip.replace('True.', 'true.').replace('False.', 'false.') _norm = _norm.replace('NULL.', 'null.').replace('None.', 'null.') return self.resolve_expression(_norm) self._log(f" Evaluando expresión: {expression_str}") try: # AGREGADO: ya no se usa eval() de Python — el evaluador maestro # (_master_eval, ver arriba) hace exactamente el mismo trabajo a # mano (sin eval, sin exec), con la misma precedencia y devolviendo # el mismo resultado. result = self._master_eval(expression_str) self._log(f" Resultado: {result}") return result except _MEDivZero: raise DivisionByZeroError(msg("op.division_zero"), **self._lc()) except _MEName as _mn: # AGREGADO: replica EXACTAMENTE el resultado que ya daba esto antes. # Con el eval() real, este caso lo lanzaba Python como NameError, # pero el bloque `except NameError as _ne:` de abajo NUNCA lo # atrapaba (la clase NameError importada de tesseract_errors no es # la NameError real de Python), así que siempre terminaba cayendo # al fallback genérico de más abajo — se reproduce ese mismo # resultado aquí, sin "arreglar" nada. raise OperationError( msg("op.invalid_operand", got=f"name '{_mn.name}' is not defined", op="expresión"), **self._lc() ) except _METypeError as _mt: # AGREGADO: mismo caso que arriba pero para TypeError — también # caía siempre al fallback genérico por el mismo shadowing. Se # replica exactamente esa misma lógica (incluido el intento previo # de mapear 'unsupported operand'/'can only' a TypeMismatchError). _es2 = _mt.message.lower() if 'unsupported operand' in _es2 or 'can only' in _es2: _tm2 = re.search(r"'([^']+)' and '([^']+)'", _mt.message) if _tm2: raise TypeMismatchError( msg("type.mismatch", expected=_tm2.group(1), got=_tm2.group(2)), **self._lc() ) raise OperationError( msg("op.invalid_operand", got=_mt.message, op="expresión"), **self._lc() ) except _MESyntax as _ms: raise OperationError( msg("op.invalid_operand", got=f"invalid syntax: {_ms.text!r}", op="expresión"), **self._lc() ) except ZeroDivisionError: raise DivisionByZeroError(msg("op.division_zero"), **self._lc()) except NameError as _ne: # Una variable no fue resuelta antes del eval → identificar cuál _undef = re.search(r"name '([^']+)' is not defined", str(_ne)) _vname = _undef.group(1) if _undef else str(_ne) raise NameError( msg("name.undefined", name=_vname), **self._lc() ) except TypeError as _te: raise InvalidOperandError( msg("op.invalid_operand", got=str(_te), op="?"), **self._lc() ) except TesseractError: raise except Exception as e: self._log(f" Error al evaluar expresión: {e}") _es = str(e).lower() if 'division' in _es or 'zero' in _es: raise DivisionByZeroError(msg("op.division_zero"), **self._lc()) if 'unsupported operand' in _es or 'can only' in _es: _tm2 = re.search(r"'([^']+)' and '([^']+)'", str(e)) if _tm2: raise TypeMismatchError( msg("type.mismatch", expected=_tm2.group(1), got=_tm2.group(2)), **self._lc() ) raise OperationError( # REPARADO: antes se pasaba op="eval", exponiendo un detalle interno # de implementación en el mensaje de error NORMAL (no de debug). El # log de arriba (self._log) ya registra el detalle completo para # cuando se necesite depurar; el mensaje de error visible ahora usa # una etiqueta genérica, sin mencionar 'eval'. msg("op.invalid_operand", got=str(e), op="expresión"), **self._lc() ) def _is_function_call(self, expression_str): """ Detecta si una cadena es una llamada a función del formato NombreFuncion(argv). AGREGADO/REPARADO: antes usaba la regex ingenua r'^[a-zA-Z_][a-zA-Z0-9_]*\\s*\\([^)]*\\)$', que exige que NO haya ningún ')' dentro de los argumentos. Eso rompía en cuanto los argumentos traían un ')' interno legítimo — por ejemplo un string con interpolación como en read(int, "entrada #(arr)") (el '#(arr)' mete un ')' antes del cierre real de read(...)), o simplemente una llamada anidada como foo(bar(x)). Ahora se valida con un escaneo de paréntesis balanceados consciente de comillas (mismo criterio que ya usa _split_args_respecting_brackets), así que un ')' dentro de un string o de una sub-llamada ya no rompe la detección. """ s = expression_str.strip() m = re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*\s*\(', s) if not m or not s.endswith(')'): return False depth = 0 in_str = False str_char = '"' n = len(s) start = m.end() - 1 # posición del '(' inicial j = start while j < n: ch = s[j] if in_str: if ch == str_char and s[j - 1] != '\\': in_str = False elif ch in ('"', "'"): in_str = True str_char = ch elif ch == '(': depth += 1 elif ch == ')': depth -= 1 if depth == 0: return j == n - 1 j += 1 return False def _execute_function_call_from_string(self, function_call_str): """Ejecuta una llamada a función desde un string y retorna su resultado""" try: # Extraer nombre de función y argumentos match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*)\)$', function_call_str.strip()) if not match: raise InvalidOperationError(f"Formato de llamada a función inválido: {function_call_str}") function_name = match.group(1) args_str = match.group(2).strip() self._log(f" Ejecutando llamada a función: '{function_name}' con args: '{args_str}'") # ══════════════════════════════════════════════════════════════════ # AGREGADO: read() usado como EXPRESIÓN de valor — # var x = read(); var x = read(int); # x = read(msg="entrada: "); x = read(int, "entrada: "); # A diferencia de read(x, int); como SENTENCIA (que sigue funcionando # exactamente igual, ver _handle_read_args), aquí NO se pasa el # nombre de variable como primer argumento — el destino ya lo da la # declaración/asignación que está resolviendo esta expresión. Sólo # el tipo y el mensaje son argumentos (ambos opcionales). # ══════════════════════════════════════════════════════════════════ if function_name == 'read': _parts = self._split_args_respecting_brackets(args_str) if args_str else [] _parts = [p.strip() for p in _parts if p.strip()] result = self._handle_read_args_as_expression(_parts) self._log(f" Resultado de '{function_call_str}': {result}") return result # Parsear argumentos call_parameters = self._parse_call_parameters({"value": args_str}) if args_str.strip() else [] # Crear contexto y ejecutar función function_context = FunctionContext(self, function_name, call_parameters) result = function_context.execute_function() self._log(f" Resultado de '{function_call_str}': {result}") return result except TesseractError: raise # propagar excepciones del sistema de errores tal cual except Exception as e: self._log(f" Error ejecutando función '{function_call_str}': {e}") raise UndefinedFunctionError( msg("func.undefined", name=function_call_str), **self._lc() ) # _parse_call_parameters: definición canónica más abajo (una sola versión unificada) def _is_valid_arithmetic_format(self, expression_str): """ Valida que una expresión tenga el formato de una operación aritmética válida (ej. lado*2, 2+x, (lado+2)*x) y que contenga al menos una variable. Usa expresiones regulares como herramienta principal. """ # 1. Patrones de las piezas (usando expresiones regulares) VAR_PATTERN = r'[a-zA-Z_][a-zA-Z0-9_]*' NUM_PATTERN = r'\d+\.?\d*' OP_PATTERN = r'[+\-*/%]' # Primero, una comprobación rápida de paréntesis balanceados if expression_str.count('(') != expression_str.count(')'): return False # 2. Tokenización: Convertimos el string en una lista de piezas token_pattern = f'({VAR_PATTERN}|{NUM_PATTERN}|{OP_PATTERN}|[()])' tokens = re.findall(token_pattern, expression_str.replace(" ", "")) if not tokens: return False # 3. Validación de la secuencia y del requisito de la variable has_variable = False # Estado esperado: 0 para operando o '(', 1 para operador o ')' expected_state = 0 paren_level = 0 for token in tokens: if re.fullmatch(VAR_PATTERN, token): if expected_state != 0: return False # Error de secuencia has_variable = True expected_state = 1 elif re.fullmatch(NUM_PATTERN, token): if expected_state != 0: return False # Error de secuencia expected_state = 1 elif re.fullmatch(OP_PATTERN, token): if expected_state != 1: return False # Error de secuencia expected_state = 0 elif token == '(': if expected_state != 0: return False # Error de secuencia paren_level += 1 elif token == ')': if expected_state != 1: return False # Error de secuencia paren_level -= 1 else: return False # Token desconocido if paren_level < 0: return False # Cierre de paréntesis sin apertura # 4. Verificación final # Debe terminar esperando un operador y con paréntesis balanceados if expected_state == 1 and paren_level == 0: return True return False def _evaluate_arithmetic_operation(self, expression_str): if not expression_str: return self._log(f" Calculando (con jerarquía) la operación: '{expression_str}'") self._log(self._current_function) # Reemplazar identificadores (params + variables) fuera de comillas _stripped = re.sub(r'"[^"]*"|\'[^\']*\'', '__', expression_str) _identifiers = set(re.findall(r'[A-Za-z_][A-Za-z0-9_]*', _stripped)) _reserved = {'true','false','null','and','or','not', 'if','else','for','while','return','break','continue'} _identifiers -= _reserved for _ident in _identifiers: _val = None # 1. Params función actual if hasattr(self, '_current_function') and self._current_function: try: _val = self.symbol_table.get_function_param_value( self._current_function, _ident) except (UndeclaredVariableError, Exception): pass # 2. Todos los frames activos if _val is None: for _fn in list(self.symbol_table.function_param_values.keys()): try: _val = self.symbol_table.get_function_param_value(_fn, _ident) break except (UndeclaredVariableError, Exception): pass # 3. Symbol table general if _val is None: try: _val = self.symbol_table.get_value(_ident) except UndeclaredVariableError: pass # Si encontró valor, resolver recursivamente si es string que apunta a otra variable if _val is not None: # Si el valor es string, resolverlo completamente: # puede ser un string literal real ("hola") o un identificador # que apunta a otra variable (b = siguiente → siguiente = 5) if isinstance(_val, str): _sv = _val.strip() # Con comillas propias → string literal real, usarlo tal cual if ((_sv.startswith('"') and _sv.endswith('"')) or (_sv.startswith("'") and _sv.endswith("'"))): pass # _val ya es el string correcto else: # Sin comillas → puede ser identificador, expresión, o número # Usar resolve_expression para obtener el valor final try: _resolved = self.resolve_expression(_sv) _val = _resolved except (TesseractError, Exception): pass # dejar _val como está _pv = json.dumps(_val) if isinstance(_val, str) else str(_val) _result = [] _i = 0 _pat = re.compile(r'\b' + re.escape(_ident) + r'\b') while _i < len(expression_str): if expression_str[_i] in ('"', "'"): _q = expression_str[_i] _end = expression_str.find(_q, _i + 1) if _end == -1: _end = len(expression_str) - 1 _result.append(expression_str[_i:_end+1]) _i = _end + 1 else: _nq = len(expression_str) for _qc in ('"', "'"): _pos = expression_str.find(_qc, _i) if _pos != -1: _nq = min(_nq, _pos) _chunk = expression_str[_i:_nq] _result.append(_pat.sub(_pv, _chunk)) _i = _nq expression_str = ''.join(_result) self._log(f" Reemplazado '{_ident}' → {_pv}") # SEGUNDO: Tokenizar incluyendo strings literales "..." y '...' tokens = re.findall(r'"[^"]*"|\'[^\']*\'|[a-zA-Z_][a-zA-Z0-9_]*|\d+\.\d*|\.\d+|\d+|[+\-*/%()]', expression_str) resolved_tokens = [] def _resolve_tok(tok): """Devuelve el valor numérico de un token identificador o literal.""" # String literal "..." o '...' — nunca es numérico if (tok.startswith('"') and tok.endswith('"')) or (tok.startswith("'") and tok.endswith("'")): raise TypeMismatchError( msg("type.mismatch", expected="number", got="string"), **self._lc_here(tok) ) if re.fullmatch(r'[a-zA-Z_][a-zA-Z0-9_]*', tok): val = None if hasattr(self, '_current_function') and self._current_function: try: val = self.symbol_table.get_function_param_value( self._current_function, tok) except (UndeclaredVariableError, Exception): pass if val is None: try: val = self.symbol_table.get_value(tok) except UndeclaredVariableError: for _fn in list(self.symbol_table.function_param_values.keys()): try: val = self.symbol_table.get_function_param_value(_fn, tok) break except (UndeclaredVariableError, Exception): pass if val is None: raise NameError( msg("name.undefined", name=tok), **self._lc_here(tok) ) if isinstance(val, bool): raise TypeMismatchError( msg("type.mismatch", expected="number", got="bool"), **self._lc_here(tok) ) if isinstance(val, (int, float)): return val if isinstance(val, str): try: return float(val) except (ValueError, TypeError): raise TypeMismatchError( msg("type.mismatch", expected="number", got="string"), **self._lc_here(tok) ) raise TypeMismatchError( msg("type.mismatch", expected="number", got=type(val).__name__), **self._lc_here(tok) ) return float(tok) # literal idx = 0 while idx < len(tokens): tok = tokens[idx] if tok == '-': # Menos unario: al inicio, o precedido por operador o '(' prev = tokens[idx - 1] if idx > 0 else None is_unary = (prev is None or prev in ('+', '-', '*', '/', '%', '(')) if is_unary and idx + 1 < len(tokens): idx += 1 try: resolved_tokens.append(-_resolve_tok(tokens[idx])) except TesseractError: raise except (ValueError, TypeError): raise InvalidOperandError( msg("op.invalid_operand", got=tokens[idx], op="negación unaria"), **self._lc_here(tokens[idx]) ) else: resolved_tokens.append('-') elif re.fullmatch(r'[a-zA-Z_][a-zA-Z0-9_]*', tok): resolved_tokens.append(_resolve_tok(tok)) elif (tok.startswith('"') and tok.endswith('"')) or \ (tok.startswith("'") and tok.endswith("'")): raise TypeMismatchError( msg("type.mismatch", expected="number", got="string"), **self._lc_here(tok) ) else: try: resolved_tokens.append(float(tok)) except ValueError: resolved_tokens.append(tok) # operador o paréntesis idx += 1 values = [] ops = [] precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '%': 2} def apply_op(): right_val = values.pop() left_val = values.pop() op = ops.pop() if not isinstance(left_val, (int, float)) or not isinstance(right_val, (int, float)): raise TypeMismatchError( msg("type.mismatch", expected=type(left_val).__name__, got=type(right_val).__name__), **self._lc_here(op) ) if op == '+': values.append(left_val + right_val) elif op == '-': values.append(left_val - right_val) elif op == '*': values.append(left_val * right_val) elif op == '/': if right_val == 0: raise DivisionByZeroError(msg('op.division_zero'), **self._lc_here(op)) values.append(left_val / right_val) elif op == '%': if right_val == 0: raise DivisionByZeroError(msg("op.modulo_zero"), **self._lc_here(op)) values.append(left_val % right_val) for token in resolved_tokens: if isinstance(token, (int, float)): values.append(token) elif token == '(': ops.append(token) elif token == ')': while ops and ops[-1] != '(': apply_op() if not ops or ops.pop() != '(': raise SyntaxError("Paréntesis no balanceados en expresión aritmética.", **self._lc()) else: while (ops and ops[-1] != '(' and precedence.get(ops[-1], 0) >= precedence.get(token, 0)): apply_op() ops.append(token) while ops: apply_op() if not values: raise OperationError(msg("op.invalid_operand", got="vacía", op="arith"), **self._lc()) result = values[0] if result == int(result): return int(result) return result # AÑADIR este método nuevo en el Interpreter: def _resolve_ternary(self, expr: str, _depth: int = 0): """ Resuelve expresión ternaria: condicion ? valor_true : valor_false Retorna el valor resuelto, o _UNRESOLVED si el expr no tiene ese formato. Comportamiento idéntico al ternario de C. """ # Buscar el '?' fuera de strings y paréntesis depth_p = 0 in_str = None q_pos = None for i, ch in enumerate(expr): if in_str: if ch == in_str and (i == 0 or expr[i-1] != '\\'): in_str = None elif ch in ('"', "'"): in_str = ch elif ch in ('(', '[', '{'): depth_p += 1 elif ch in (')', ']', '}'): depth_p -= 1 elif ch == '?' and depth_p == 0 and in_str is None: q_pos = i break if q_pos is None: return _UNRESOLVED # Buscar el ':' que separa true/false, fuera de strings y paréntesis depth_p = 0 in_str = None c_pos = None for i, ch in enumerate(expr[q_pos+1:], start=q_pos+1): if in_str: if ch == in_str and expr[i-1] != '\\': in_str = None elif ch in ('"', "'"): in_str = ch elif ch in ('(', '[', '{'): depth_p += 1 elif ch in (')', ']', '}'): depth_p -= 1 elif ch == ':' and depth_p == 0 and in_str is None: c_pos = i break if c_pos is None: return _UNRESOLVED cond_str = expr[:q_pos].strip() true_str = expr[q_pos+1:c_pos].strip() false_str = expr[c_pos+1:].strip() if not cond_str or not true_str or not false_str: return _UNRESOLVED # Evaluar condición try: cond_val = self._evaluate_condition(cond_str) except TesseractError: raise except Exception: # Si no se puede evaluar como condición, no es un ternario válido return _UNRESOLVED # Resolver el branch correcto branch = true_str if cond_val else false_str return self.resolve_expression(branch, _depth + 1) def _format_interpolated_value(self, value, expr_text=""): """ Formatea el resultado de un #( ... ) ya resuelto, para insertarlo dentro del string. Usa las mismas reglas de formato que el resto del lenguaje (_fmt_val / format_print) para que #(x) imprima igual que print(x): - StructInstance -> format_print(...) - string -> tal cual, SIN comillas - resto (int, float, bool, null, list, tuple, dict, etc.) -> _fmt_val """ if isinstance(value, StructInstance): return value.format_print(expr_text or value.struct_name) if isinstance(value, str): return value return _fmt_val(value) def _interpolate_hash_string(self, s): """ Soporte de formateo de strings (string interpolation): #( expresion ). Detecta UNICAMENTE dentro de un string ya desquotado (es decir, ya dentro de las comillas del literal) la presencia de #( ... ), resuelve lo que esté dentro con resolve_expression (variables, aritmetica, llamadas a funcion, lo que sea) y sustituye #( ... ) por el valor ya resuelto: var x = 5; print("x = #(x)"); -> x = 5 print("x = #(x+2)"); -> x = 7 Si el valor resuelto es un string, se inserta tal cual (sin comillas). Si no hay ningún '#(' en el string, se devuelve sin tocar (no-op), para no afectar a ningún string que ya funcionaba antes. """ if not isinstance(s, str) or '#(' not in s: return s out = [] i = 0 n = len(s) while i < n: if s[i] == '#' and i + 1 < n and s[i + 1] == '(': close = self._find_matching_close(s, i + 1) if close is not None: inner_expr = s[i + 2:close] try: resolved = self.resolve_expression(inner_expr.strip()) except TesseractError: raise out.append(self._format_interpolated_value(resolved, inner_expr.strip())) i = close + 1 continue # No se encontró cierre -> dejar tal cual (no es un #(...) válido) out.append(s[i]) i += 1 continue out.append(s[i]) i += 1 return ''.join(out) def resolve_expression(self, expression_str, _depth=0): """ Resolvedor unificado. Maneja TODOS los casos en orden de prioridad: 1. Valor Python no-string -> devolver tal cual 2. Vacio/None -> None 3. null / NULL / None literal -> None 4. true / false literal -> bool 5. Entero literal -> int 6. Decimal literal -> float (ANTES del check de punto) 7. String literal "..." / '...' -> str (escapes procesados) 8. Array literal [...] -> list 9. new ClassName(args) -> instanciacion OOP 10. Expresion con punto (NO float) -> _resolve_dot_chain 11. Llamada a funcion simple name(args) -> _execute_function_call_from_string 12. Identificador simple -> param o symbol_table 13. Aritmetica pura (sin comparaciones) -> _evaluate_arithmetic_operation 14. Todo lo demas -> evaluate_expression _depth: protección interna contra recursión infinita. """ # Guard anti-recursión if _depth > 80: self._log(f" [WARN] resolve_expression: profundidad {_depth} alcanzada para: {str(expression_str)[:80]!r}") return expression_str # 1. Valor Python no-string if not isinstance(expression_str, str): return expression_str expr = expression_str.strip() # 2. Vacio if not expr: return None # 3. null literal if expr in ('null', 'NULL'): return None # 3.1. String literal — comillas dobles o simples = string, SIEMPRE # "a" no es la variable a. 'hola' no es la variable hola. # Esta regla es universal y va antes de cualquier otra cosa. # AGREGADO: salvo que sea una concatenación con ' . ' (punto con # espacios) — en ese caso debe caer más abajo hasta # _evaluate_concatenated_string, igual que ya hacía el paso 7. # Sin este guard, "Hola " . nombre . ", tienes " . edad . " años." # se trataba como un string literal plano (solo se quitaban las # comillas de los extremos) y nunca se resolvían ni concatenaban # las variables. # AGREGADO también: salvo que contenga '..' — un RANGO de # strings como "a".."z" también arranca y termina con comilla # (parece un string literal plano), así que sin esta exclusión # este paso lo interceptaba primero y sólo quitaba las comillas # de los extremos, dejando el texto roto 'a".."z' en vez de # dejar que el paso 3.5 (Range literal) lo reconociera como # rango. Igual que el paso 6 ya excluye '..' para los decimales. # Si '..' resulta no ser un rango válido (string normal que # sólo contiene esos puntos como texto), el paso 7 más abajo # lo captura igual como string plano — no se pierde nada. if ((expr.startswith('"') and expr.endswith('"') and len(expr) >= 2) or (expr.startswith("'") and expr.endswith("'") and len(expr) >= 2)) and \ ' . ' not in expr and '..' not in expr: return self._interpolate_hash_string(expr[1:-1]) # 3.2. Ternario: condicion ? valor_true : valor_false # Detectar antes de cualquier otra cosa para que el ? no confunda if '?' in expr and ':' in expr: tern = self._resolve_ternary(expr, _depth) if tern is not _UNRESOLVED: return tern # 3.5. Range literal: start..end # Debe revisarse antes de intentar float/int para que 1..5 no confunda if '..' in expr: rng = self._try_parse_range(expr) if rng is not None: return rng # 4. bool literals if expr == 'true': return True if expr == 'false': return False # 5. Entero literal (incluyendo negativo sin espacios) try: return int(expr) except ValueError: pass # 6. Decimal literal — ANTES del check de punto para que 3.14 no vaya # a _resolve_dot_chain. Excluir si contiene '..' (rango) o '(' (expresión) try: if '.' in expr and '..' not in expr and '(' not in expr and ')' not in expr: return float(expr) except ValueError: pass # 7. String literal (doble o simple comilla) — solo si no es concatenación # AGREGADO: excluir '..' por la misma razón que el paso 3.1 — un # rango de string como "a".."z" también arranca/termina en comilla, # así que sin esta exclusión este paso (que es el fallback si el # paso 3.5 no lo capturó) le quitaba las comillas de los extremos # dejando el texto roto 'a".."z' en vez de dejarlo como rango. if ((expr.startswith('"') and expr.endswith('"')) or (expr.startswith("'") and expr.endswith("'"))) and \ ' . ' not in expr and '..' not in expr: inner = expr[1:-1] inner = (inner.replace('\\n', '\n').replace('\\t', '\t') .replace('\\r', '\r').replace('\\"', '"') .replace("\\'", "'").replace('\\\\', '\\')) return self._interpolate_hash_string(inner) # 8. Array literal [...] if expr.startswith('[') and expr.endswith(']'): inner = expr[1:-1].strip() if not inner: return [] raw_items = [self.resolve_expression(i.strip(), _depth + 1) for i in self._split_args_respecting_brackets(inner)] return _expand_collection_items(raw_items) # 8.4. (expr).method — e.g. (0..10).length, (1..5).toarray() # Detectar antes del check de tupla para no confundirlos if expr.startswith('(') and ').' in expr: # Encontrar el ) que cierra la expresión principal close = self._find_matching_close(expr, 0) if close is not None and close < len(expr) - 1 and expr[close + 1] == '.': inner_expr = expr[1:close] tail_chain = expr[close + 2:] # método(s) tras el punto try: inner_val = self.resolve_expression(inner_expr, _depth + 1) if inner_val is not None and tail_chain: val_type = _get_value_type(inner_val) first_m = tail_chain.split('(')[0] if val_type in _CORE_TYPE_METHODS and \ first_m in _CORE_TYPE_METHODS[val_type]: result, _ = self._execute_type_method_chain( inner_expr, tail_chain, inner_val, False) return result # No es un type-method conocido — devolver el valor interior return inner_val except TesseractError: raise except Exception: pass # no es este patrón, seguir # 8.5. Tuple literal (val1, val2, ...) if expr.startswith('(') and expr.endswith(')'): inner = expr[1:-1].strip() if inner: items = self._split_args_respecting_brackets(inner) if len(items) > 1 or inner.endswith(','): try: raw_items = [self.resolve_expression(it.strip(), _depth + 1) for it in items] return tuple(_expand_collection_items(raw_items)) except TesseractError: raise except Exception: pass # 8.6. Dict literal {...} if expr.startswith('{') and expr.endswith('}'): inner = expr[1:-1].strip() if not inner: return {} try: import ujson as _uj return _uj.loads(expr) except TesseractError: raise except Exception: pass # 9.5. Acceso indexado: var[i], var[i:j], p1.field[i], p1.field[i:j] # También soporta encadenado: var[i].type var[i:j].length() idx_match = re.match(r'^([\w]+(?:\.[\w]+)*)\[(.+)\](\.[\w.()]+)?$', expr, re.DOTALL) if idx_match: base_with_field = idx_match.group(1) bracket_content = idx_match.group(2) tail_chain = idx_match.group(3) # e.g. ".type" o ".length()" o None # Verificar que el bracket_content no contiene '[' sin cerrar (evitar falsos positivos) # Solo si la base existe como variable o campo de struct base_root = base_with_field.split('.')[0] base_known = False try: # AGREGADO: antes usaba symbol_table.get_value directo, que # nunca reconoce un PARÁMETRO de función como base válida # (ej. 'arreglo' en function ord_burbuja(arreglo){...}). # Eso hacía que 'arreglo[j]' se saltara por completo este # camino de acceso indexado y el texto crudo terminara # colándose como si fuera un string — ver # _resolve_root_value_for_read. self._resolve_root_value_for_read(base_root) base_known = True except UndeclaredVariableError: pass if base_known: result = self._evaluate_array_access(f"{base_with_field}[{bracket_content}]") # Si hay cadena de tipo (.type, .length(), etc.) aplicarla if tail_chain: chain = tail_chain[1:] # quitar el punto inicial val_type = _get_value_type(result) if val_type in _CORE_TYPE_METHODS: first_m = chain.split('(')[0] if first_m in _CORE_TYPE_METHODS[val_type]: result, _ = self._execute_type_method_chain( base_with_field, chain, result, False) return result # 9. new ClassName(args) new_m = re.match(r'^new\s+([A-Za-z_]\w*)\s*\((.*)\)$', expr, re.DOTALL) if new_m: raw_args = new_m.group(2).strip() args = self._parse_call_parameters({'value': raw_args}) if raw_args else [] return self._instantiate_object(new_m.group(1), args) # 9.5 AGREGADO: literal numérico seguido de .metodo, ej. '1.method', # '3.14.round(1)', '(-2).abs' (sin paréntesis de agrupación aquí). # _resolve_dot_chain (paso 10) exige que el lado izquierdo sea un # identificador (letra/guion bajo), así que un número literal # como '1.method' nunca calzaba ahí — esto permite pedir la lista # de métodos (u otro método core) directamente sobre un literal, # sin necesitar declarar una variable primero: # print(1.method); # print(3.14.round(1)); _lit_dot_m = re.match(r'^(-?\d+\.\d+|-?\d+)\.([A-Za-z_]\w*)\s*(\((.*)\))?$', expr, re.DOTALL) if _lit_dot_m: _num_txt, _method_name, _has_parens, _args_txt = _lit_dot_m.groups() _num_val = float(_num_txt) if '.' in _num_txt else int(_num_txt) _val_type = _get_value_type(_num_val) if _val_type in _CORE_TYPE_METHODS and _method_name in _CORE_TYPE_METHODS[_val_type]: _chain_str = f"{_method_name}({_args_txt if _args_txt else ''})" result, _ = self._execute_type_method_chain( f"", _chain_str, _num_val, True) return result # 9.6 AGREGADO: mismo caso que 9.5 pero para los demás literales # (string, booleano, array, tupla, dict) seguidos de .metodo, # ej. "asd".type, true.type, [].type, [1,2,3].length, # (1,2).type, {"a":1}.type — ver _extract_leading_literal_dot_call. _lit_call = self._extract_leading_literal_dot_call(expr) if _lit_call: _literal_text, _lit_method_name, _lit_args_txt = _lit_call _lit_val = self.resolve_expression(_literal_text) _val_type = _get_value_type(_lit_val) if _val_type in _CORE_TYPE_METHODS: if _lit_method_name in _CORE_TYPE_METHODS[_val_type]: _chain_str = f"{_lit_method_name}({_lit_args_txt if _lit_args_txt else ''})" result, _ = self._execute_type_method_chain( "", _chain_str, _lit_val, True) return result # AGREGADO: método no definido para ese tipo -> error claro, # no NameError confuso ni AttributeError crudo de Python. raise InvalidOperationError( f"'{_val_type}' no tiene un método o atributo llamado " f"'{_lit_method_name}'. Usa '.method' sobre un valor de " f"ese tipo para ver la lista de métodos disponibles.", **self._lc_here(expr) ) # 10. Expresion con punto (modulo, OOP, tipo core, concatenacion) # Solo si hay punto Y no es decimal ya manejado en paso 6 if '.' in expr: resolved = self._resolve_dot_chain(expr) if resolved is not _UNRESOLVED: return resolved # 11. Llamada a funcion simple name(args) if self._is_function_call(expr): return self._execute_function_call_from_string(expr) # 12. Identificador simple -> param de funcion o symbol_table if re.fullmatch(r'[A-Za-z_]\w*', expr): # Palabras reservadas — no son variables if expr not in ('true', 'false', 'null', 'and', 'or', 'not', 'if', 'else', 'for', 'while', 'return', 'break', 'continue'): # 1. Params de la función actual if hasattr(self, '_current_function') and self._current_function: fps = self.symbol_table.get_function_params(self._current_function) if expr in fps: try: return self.symbol_table.get_function_param_value( self._current_function, expr) except UndeclaredVariableError: pass # 2. Symbol table (scope local/global) try: return self.symbol_table.get_value(expr) except UndeclaredVariableError: pass # 3. Todos los frames activos (llamadas anidadas) for _fn in list(self.symbol_table.function_param_values.keys()): try: return self.symbol_table.get_function_param_value(_fn, expr) except (UndeclaredVariableError, Exception): pass # 4. No encontrado → NameError raise NameError( msg("name.undefined", name=expr), **self._lc_here(expr) ) # 13. Resolver llamadas embebidas primero expr_w = self._resolve_embedded_function_calls(expr) # 14. Aritmetica pura (shunting-yard, sin eval) if self._is_valid_arithmetic_format(expr_w): return self._evaluate_arithmetic_operation(expr_w) # 15. Comparaciones, logica, expresiones complejas -> evaluate_expression return self.evaluate_expression(expr_w) def _resolve_dot_chain(self, expr: str): """ Resuelve cualquier expresión que contenga punto. Orden de prioridad estricto: 1. ModuleInstance.method(args) — instancia nativa en variable 2. mod.func(args) — módulo cargado con función 3. mod.VAR — módulo cargado con export 4. ClassInstance.field — atributo de objeto OOP 5. ClassInstance.method(args) — método de objeto OOP 6. var.typeMethod().chain — interfaz de tipo (CORE) 7. Concatenación con punto — último recurso """ expr = expr.strip() # ── Detectar la parte izquierda del primer punto ─────────────────────── # Extraer nombre antes del primer punto fuera de paréntesis left = self._extract_left_of_dot(expr) # SOLO estas líneas 1610-1612: if not left: # Verificar si hay una llamada a módulo cargado embebida dentro de la expresión # Ej: (-b + math.sqrt(discriminante)) / (2*a) mod_embedded = re.search( r'([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(', expr) if mod_embedded and self.module_loader.is_loaded(mod_embedded.group(1)): resolved = self._resolve_embedded_function_calls(expr) # Sustituir parámetros de función if hasattr(self, '_current_function') and self._current_function: for p in self.symbol_table.get_function_params(self._current_function): try: v = self.symbol_table.get_function_param_value(self._current_function, p) _pv3 = str(v) resolved = re.sub(r'\b' + re.escape(p) + r'\b', lambda m, s=_pv3: s, resolved) except TesseractError: raise except Exception: pass # Sustituir variables normales del scope for var in set(re.findall(r'[a-zA-Z_][a-zA-Z0-9_]*', resolved)): try: v = self.symbol_table.get_value(var) if isinstance(v, (int, float)): _pv4 = str(v) resolved = re.sub(r'\b' + re.escape(var) + r'\b', lambda m, s=_pv4: s, resolved) except TesseractError: raise except Exception: pass # eval maneja doble negativo (--3) correctamente, a diferencia del tokenizador try: result = eval(resolved, {"__builtins__": {}}, {}) if isinstance(result, float) and result == int(result): return int(result) return result except TesseractError: raise except Exception: pass # Sin módulo embebido → es concatenación real, comportamiento original # No hay identificador claro a la izquierda → concatenación return self._evaluate_concatenated_string(expr) rest = expr[len(left) + 1:] # lo que sigue después del primer punto # ── 0. this.field / this.method(args) — acceso a instancia actual ────── # Debe ir ANTES que cualquier lookup de módulo para que "this" nunca # llegue a module_loader.is_loaded("this") if left == "this" and self._current_instance is not None: # this.field — acceso a atributo field_m = re.match(r'^([A-Za-z_]\w*)$', rest) if field_m: return self._current_instance.get_attribute(field_m.group(1)) # this.method(args) — llamada a método sobre sí mismo call_m = re.match(r'^([A-Za-z_]\w*)\s*\((.*)\)$', rest, re.DOTALL) if call_m: method_name = call_m.group(1) raw_args = call_m.group(2) args = self._parse_call_parameters({"value": raw_args}) if raw_args.strip() else [] try: method_info, origin = self.object_table.lookup_method( self._current_instance.class_name, method_name) from interpre import MethodContext as _MC return _MC(self, method_info, self._current_instance, origin, args).execute() except (UndeclaredVariableError, InterpreterError): pass return _UNRESOLVED try: obj = self.symbol_table.get_value(left) if isinstance(obj, ModuleInstance): # Parsear el método y args del resto m = re.match(r'^([A-Za-z_]\w*)\s*\((.*)\)(.*)$', rest, re.DOTALL) if m: method_name = m.group(1) raw_args = m.group(2) tail = m.group(3).strip() args = self._parse_call_parameters({"value": raw_args}) if raw_args.strip() else [] args = [a.native_obj if isinstance(a, ModuleInstance) else a for a in args] result = obj.call_method(method_name, args, self) # Si hay más cadena después (.algo) seguir resolviendo if tail and tail.startswith('.'): return self._resolve_dot_chain(f"__r__.{tail[1:]}".replace( '__r__', str(result))) return result except (UndeclaredVariableError, InterpreterError, AttributeError): pass # ── 2. mod.func(args) — módulo con función ──────────────────────────── mod_call = re.match( r'^([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\((.*)\)$', expr, re.DOTALL) if mod_call: mod_name, func_name, raw_args = mod_call.groups() if self.module_loader.is_loaded(mod_name): args = self._parse_call_parameters({"value": raw_args}) if raw_args.strip() else [] try: fn = self.module_loader.get_function(mod_name, func_name) return fn.call(args, self) except ModuleError as e: raise InterpreterError(str(e), **self._lc()) # ── 3. mod.VAR — módulo con export ──────────────────────────────────── mod_var = re.match(r'^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$', expr) if mod_var: mod_name, var_name = mod_var.groups() if self.module_loader.is_loaded(mod_name): try: return self.module_loader.get_export_value(mod_name, var_name) except ModuleError: pass # no exporta eso — seguir al siguiente caso # ── 3.5. StructInstance.field o StructInstance.field.subfield ─────────── try: obj = self.symbol_table.get_value(left) if isinstance(obj, StructInstance): # Puede ser: "field", "field.subfield", "field[i]", "field[i:j]", "field.sub[i]" # Separar la parte de bracket si existe bracket_m = re.match(r'^([\w.]+)(\[.+\])(\.[\w.()]+)?$', rest) if bracket_m: field_path_part = bracket_m.group(1) bracket_part = bracket_m.group(2) tail_part = bracket_m.group(3) # Navegar al campo field_val = self._get_struct_field_by_path(obj, field_path_part) # Aplicar acceso indexado inner_content = bracket_part[1:-1] # quitar [ ] result = self._evaluate_array_access_on_value( field_val, inner_content, f"{left}.{field_path_part}") if tail_part: chain = tail_part[1:] val_type = _get_value_type(result) if val_type in _CORE_TYPE_METHODS: result, _ = self._execute_type_method_chain( f"{left}.{field_path_part}", chain, result, False) return result return self._get_struct_field_by_path(obj, rest) except (UndeclaredVariableError, InterpreterError): pass # ── 4 & 5. ClassInstance.field o ClassInstance.method(args) ─────────── try: obj = self.symbol_table.get_value(left) if isinstance(obj, ClassInstance): # ¿Es llamada a método? m = re.match(r'^([A-Za-z_]\w*)\s*\((.*)\)$', rest, re.DOTALL) if m: method_name = m.group(1) raw_args = m.group(2) args = self._parse_call_parameters({"value": raw_args}) if raw_args.strip() else [] from interpre import MethodContext method_info, origin = self.object_table.lookup_method( obj.class_name, method_name) return MethodContext(self, method_info, obj, origin, args).execute() # ¿Es acceso a atributo? field_m = re.match(r'^([A-Za-z_]\w*)$', rest) if field_m: return obj.get_attribute(field_m.group(1)) except (UndeclaredVariableError, InterpreterError): pass # ── 6. Interfaz de tipo (CORE) ──────────────────────────────────────── # Solo si el lado izquierdo es una variable con valor conocido # y el primer método existe en la tabla del tipo # REPARADO: antes todo esto (incluida la ejecución real de # _execute_type_method_chain) estaba dentro de un único try/except que # atrapaba InvalidOperationError. Eso significaba que CUALQUIER error # real lanzado por un método (ej. '.mut' mal usado, un push() que # revienta, un tipo incompatible) se silenciaba con 'pass' y el código # caía al fallback de concatenación de strings (sección 7), produciendo # basura en vez de mostrar el error real — esto es lo que causaba # resultados corruptos tipo texto pegado en vez de un error claro. # Ahora sólo se atrapa el fallo de RESOLVER el símbolo (UndeclaredVariableError, # que es la señal legítima de "prueba el siguiente caso, ej. parámetro"); # cualquier InvalidOperationError de la ejecución real se deja propagar. # Literales reservados: null, true, false no están en symbol_table # pero tienen core methods. Se interceptan aquí antes del lookup. if left in ('null', 'NULL', 'None'): _first_m = rest.split('(')[0].split('.')[0] if _first_m in _CORE_TYPE_METHODS.get('null', {}): result, _ = self._execute_type_method_chain(left, rest, None, True) return result raise InvalidOperationError( f"'null' no tiene el método '{_first_m}'.", **self._lc_here(expr) ) if left in ('true', 'false'): _bv = (left == 'true') _first_m = rest.split('(')[0].split('.')[0] if _first_m in _CORE_TYPE_METHODS.get('bool', {}): result, _ = self._execute_type_method_chain(left, rest, _bv, True) return result raise InvalidOperationError( f"'bool' no tiene el método '{_first_m}'.", **self._lc_here(expr) ) sym = None try: sym = self.symbol_table.get_symbol(left) origin_value = sym.value is_const = sym.is_const value_type = _get_value_type(origin_value) first_method = rest.split('(')[0].split('.')[0] except (UndeclaredVariableError, AttributeError): sym = None if sym is not None and first_method in _CORE_TYPE_METHODS.get(value_type, {}): result, _ = self._execute_type_method_chain( left, rest, origin_value, is_const) self._log(f"[TypeMethod] {left}.{rest} = {result}") return result # AGREGADO: si 'sym' SÍ se resolvió y su tipo SÍ es un tipo core # conocido (incluye 'range'), pero el método pedido no existe en la # tabla de ese tipo, es un error real y claro — no debe caer en # silencio al fallback de concatenación de strings (que antes producía # basura o un NameError confuso sobre el propio nombre del método). elif sym is not None and value_type in _CORE_TYPE_METHODS: raise InvalidOperationError( f"'{value_type}' no tiene un método o atributo llamado '{first_method}'. " f"Usa '{left}.method' para ver la lista de métodos disponibles para '{value_type}'.", **self._lc_here(expr) ) # También buscar en parámetros de función if hasattr(self, '_current_function') and self._current_function: try: origin_value = self.symbol_table.get_function_param_value( self._current_function, left) value_type = _get_value_type(origin_value) first_method = rest.split('(')[0].split('.')[0] if first_method in _CORE_TYPE_METHODS.get(value_type, {}): # REPARADO: pasar param_frame para que 'push'/'pop'/etc. # (mutable_default) y '.mut' persistan de vuelta en # function_param_values en vez de intentar (y fallar en # silencio) symbol_table.set_value sobre un parámetro. result, _ = self._execute_type_method_chain( left, rest, origin_value, False, param_frame=self._current_function) return result except TesseractError: raise except (UndeclaredVariableError, InvalidOperationError): pass # ── 7. Último recurso: concatenación con punto ──────────────────────── return self._evaluate_concatenated_string(expr) def _extract_leading_literal_dot_call(self, expr: str): """ AGREGADO: detecta si 'expr' ES un literal —string, booleano, array, tupla o dict— seguido INMEDIATAMENTE de '.metodo' (con o sin paréntesis de llamada), respetando comillas/corchetes/llaves/ paréntesis anidados para saber exactamente dónde termina el literal. Es el mismo problema que ya se resolvió para números en el paso 9.5 ('1.method'), pero generalizado a los demás tipos: "asd".type true.type false.isBool() [].type [1,2,3].length (1,2).type {"a":1}.type Sin esto, estos casos nunca llegaban a _resolve_dot_chain porque éste exige que el lado izquierdo del punto sea un identificador (letra/guion bajo) — un literal no calza ahí y terminaba cayendo en rutas de fallback que daban NameError confusos o errores crudos de Python (ej. "'bool' object has no attribute 'type'"). Devuelve (literal_text, method_name, args_text_or_None) si aplica, o None si 'expr' no tiene esta forma. """ s = expr.strip() if not s: return None n = len(s) i = None # 1. Booleanos: 'true' / 'false' como palabra completa for _kw in ('true', 'false'): if s.startswith(_kw) and s[len(_kw):len(_kw) + 1] == '.': i = len(_kw) break if i is None and s[0] in ('"', "'"): # 2. String literal quote = s[0] j = 1 closed = False while j < n: if s[j] == '\\': j += 2 continue if s[j] == quote: j += 1 closed = True break j += 1 if not closed: return None i = j elif i is None and s[0] == '[': # 3. Array literal depth, j, in_str = 0, 0, None closed = False while j < n: c = s[j] if in_str: if c == '\\': j += 2 continue if c == in_str: in_str = None elif c in ('"', "'"): in_str = c elif c == '[': depth += 1 elif c == ']': depth -= 1 if depth == 0: j += 1 closed = True break j += 1 if not closed: return None i = j elif i is None and s[0] == '{': # 4. Dict literal depth, j, in_str = 0, 0, None closed = False while j < n: c = s[j] if in_str: if c == '\\': j += 2 continue if c == in_str: in_str = None elif c in ('"', "'"): in_str = c elif c == '{': depth += 1 elif c == '}': depth -= 1 if depth == 0: j += 1 closed = True break j += 1 if not closed: return None i = j elif i is None and s[0] == '(': # 5. Tupla literal: (a, b, ...) — exige al menos una coma al # nivel superior para no confundir con una simple # agrupación de paréntesis, ej. (2 + 3).method NO es tupla. depth, j, in_str = 0, 0, None closed, has_comma_top = False, False while j < n: c = s[j] if in_str: if c == '\\': j += 2 continue if c == in_str: in_str = None elif c in ('"', "'"): in_str = c elif c == '(': depth += 1 elif c == ')': depth -= 1 if depth == 0: j += 1 closed = True break elif c == ',' and depth == 1: has_comma_top = True j += 1 if not (closed and has_comma_top): return None i = j if i is None: return None rest = s[i:] m = re.match(r'^\.([A-Za-z_]\w*)\s*(?:\((.*)\))?$', rest, re.DOTALL) if not m: return None method_name = m.group(1) args_text = m.group(2) # None si no había paréntesis, "" si estaban vacíos return s[:i], method_name, args_text def _extract_left_of_dot(self, expr: str) -> str: """ Extrae el identificador a la izquierda del primer punto fuera de paréntesis. 'math.sqrt(x)' → 'math' 'obj.field' → 'obj' '"hola".upper' → '' (no es identificador) """ m = re.match(r'^([A-Za-z_]\w*)\.', expr) if m: return m.group(1) return '' def _resolve_range_value(self, value_str): """ Resuelve un valor que puede ser una variable o un literal. Retorna el valor resuelto. """ value_str = value_str.strip() # Si es un string literal con comillas, quitarlas if value_str.startswith('"') and value_str.endswith('"'): return value_str[1:-1] # Intentar obtener como variable try: return self.symbol_table.get_value(value_str) except UndeclaredVariableError: pass # Intentar obtener como parámetro de función actual if hasattr(self, '_current_function') and self._current_function: try: return self.symbol_table.get_function_param_value( self._current_function, value_str ) except UndeclaredVariableError: pass # Intentar convertir a número try: if '.' in value_str: return float(value_str) else: return int(value_str) except ValueError: pass # Intentar resolver como expresión compuesta (ej: arr.length, obj.campo, func()) try: resolved = self.resolve_expression(value_str) if resolved is not None and not isinstance(resolved, str): return resolved # Si devolvió un string distinto al original, también es útil if isinstance(resolved, str) and resolved != value_str: return resolved except TesseractError: raise except Exception: pass # Si no es nada de lo anterior, retornar como string return value_str def _validate_and_parse_range(self, range_str, for_loop=False): """ Valida y parsea un rango, soportando variables. Args: range_str: String del rango (ej: "1..10", "start..end", "a..z") for_loop: Si True, solo permite rangos int (no float ni string) Returns: tuple: (start, end, range_type) donde range_type es 'int', 'float' o 'string' Raises: ValueError: Si el rango no es válido """ if ".." not in range_str: raise ValueError(f"Formato de rango inválido: '{range_str}' (debe contener '..')") parts = range_str.split("..") if len(parts) != 2: raise ValueError(f"Formato de rango inválido: '{range_str}' (debe tener exactamente dos valores)") # Resolver los valores (pueden ser variables) start_value = self._resolve_range_value(parts[0]) end_value = self._resolve_range_value(parts[1]) # Determinar el tipo de rango start_type = type(start_value).__name__ end_type = type(end_value).__name__ # VALIDACIÓN 1: Ambos valores deben ser del mismo tipo base if isinstance(start_value, (int, float)) and isinstance(end_value, (int, float)): # Rango numérico if isinstance(start_value, float) or isinstance(end_value, float): range_type = 'float' start_value = float(start_value) end_value = float(end_value) else: range_type = 'int' start_value = int(start_value) end_value = int(end_value) # Validar que start <= end if start_value > end_value: raise ValueError(f"Rango inválido: el inicio ({start_value}) es mayor que el fin ({end_value})") elif isinstance(start_value, str) and isinstance(end_value, str): # Rango de strings (caracteres) range_type = 'string' # Validar que sean caracteres únicos if len(start_value) != 1 or len(end_value) != 1: raise ValueError(f"Rango de string inválido: '{start_value}'..'{end_value}' (deben ser caracteres únicos)") # Validar que start <= end en orden alfabético if ord(start_value) > ord(end_value): raise ValueError(f"Rango de string inválido: '{start_value}' viene después de '{end_value}' en el alfabeto") else: raise ValueError(f"Rango inválido: tipos incompatibles ({start_type} .. {end_type})") # VALIDACIÓN 2: Si es para un for loop, solo permitir int if for_loop and range_type != 'int': raise ValueError(f"Rango inválido para bucle 'for': solo se permiten rangos de enteros (int), no '{range_type}'") return start_value, end_value, range_type def _generate_range_values(self, start, end, range_type): """ Genera los valores de un rango según su tipo. Returns: list: Lista de valores en el rango """ if range_type == 'int': return list(range(start, end + 1)) elif range_type == 'float': # Rango con paso de 0.1 values = [] current = start step = 0.1 # Redondear para evitar problemas de precisión while round(current, 10) <= round(end, 10): values.append(round(current, 1)) current += step return values elif range_type == 'string': # Rango de caracteres values = [] for code in range(ord(start), ord(end) + 1): values.append(chr(code)) return values return [] # ────────────────────────────────────────────────────────────────────── # AGREGADO: validación de tipo declarado (escalar y colecciones tipadas) # ────────────────────────────────────────────────────────────────────── def _validate_declared_type_value(self, declared_type, value, node, raw_value_for_lc): """ Valida `value` contra `declared_type` y devuelve el valor final que debe almacenarse (puede ser el mismo `value`, o una versión coaccionada si aplica conversión implícita int↔float↔bool). AGREGADO: coerción implícita estilo C/Java entre tipos "afines": - int ← float : trunca la parte decimal (2.5 → 2) - float ← int : promueve a float (2 → 2.0) - int ← bool : true → 1, false → 0 - bool ← int : 0 → false, 1 → true (cualquier otro entero sigue siendo un conflicto de tipo) Esto aplica tanto a declaraciones/asignaciones escalares como a cada elemento hoja de una colección tipada (recursivamente, incluyendo colecciones anidadas dentro de ella). Si no hay coerción posible y el tipo no coincide, se lanza TypeExplicitConflictError, igual que el comportamiento previo. """ if declared_type in ('dynamic', 'any') or value is None: return value actual = _get_value_type(value) # Coincidencia directa: no hace falta tocar nada. if actual == declared_type: return value # ── AGREGADO: coerción numérica/booleana implícita (int/float/bool) ── coerced, changed = _coerce_scalar_value(value, declared_type) if changed: return coerced # ── Colección tipada (ya existente) — ahora también coacciona ──── if declared_type not in ('array', 'tuple', 'dict') and actual in ('array', 'tuple', 'dict'): ok, coerced_collection, bad_value, bad_type = _coerce_collection_element_types(value, declared_type) if ok: return coerced_collection raise TypeExplicitConflictError( msg("type.explicit_conflict", declared=declared_type, got=bad_type), **self._lc_at(node, str(raw_value_for_lc) if raw_value_for_lc is not None else "") ) # ── Conflicto simple (comportamiento previo, sin cambios) ──────── raise TypeExplicitConflictError( msg("type.explicit_conflict", declared=declared_type, got=actual), **self._lc_at(node, str(raw_value_for_lc) if raw_value_for_lc is not None else "") ) def _enforce_collection_type_for_var(self, var_name, new_value, method_name=None): """ AGREGADO: si `var_name` fue declarada con un tipo escalar/nativo pero su valor es una colección (array/tuple/dict) — es decir, es una "colección tipada" — valida y coacciona `new_value` (el nuevo estado de la colección tras push/insert/unshift/concat/.mut/etc.) para que cada elemento respete ese tipo (con la misma coerción implícita int↔float↔bool que aplica a variables escalares). Devuelve el valor final que debe guardarse en la variable (puede ser el mismo `new_value`, o una copia con elementos coaccionados). No hace nada (devuelve `new_value` tal cual) si la variable no existe (parámetro/temporal), si su tipo declarado es dinámico/contenedor genérico, o si el nuevo valor no es colección. """ try: sym = self.symbol_table.get_symbol(var_name) except UndeclaredVariableError: return new_value declared = sym.declared_type if declared in ('dynamic', 'any', None, 'array', 'tuple', 'dict'): return new_value if not isinstance(new_value, (list, tuple, dict)): return new_value ok, coerced_collection, bad_value, bad_type = _coerce_collection_element_types(new_value, declared) if ok: return coerced_collection raise TypeExplicitConflictError( msg("type.explicit_conflict", declared=declared, got=bad_type), **self._lc_here(var_name) ) # ══════════════════════════════════════════════════════════════════════ # AGREGADO: detección compartida de "esto necesita resolve_expression()" # ══════════════════════════════════════════════════════════════════════ # Antes había 5 copias casi idénticas de esta misma lista de condiciones # (en VariableDeclaration, VariableAsignement, ConstantDeclaration, # asignación a índice y asignación a campo de struct), y ninguna # detectaba comparaciones/lógicos/pertenencia — por eso algo como # "12 in arr" o "x == 5" se guardaba TAL CUAL como el texto plano en # vez de evaluarse a un bool real. Ahora es una sola función compartida # (superconjunto de las 5 versiones anteriores, así que sigue cubriendo # exactamente los mismos casos de antes) que además detecta: # - comparaciones: ==, !=, <=, >=, <, > # - lógicos: &&, ||, and, or, not # - pertenencia: in, !in def _needs_expression_resolve(self, raw_value) -> bool: if not isinstance(raw_value, str): return False return ( # AGREGADO: '[' — acceso indexado (arr[i]) o literal de array/dict # ([1,2,3], {...}) sin ningún operador aritmético dentro (ej. # 'arreglo[j]') antes NO disparaba needs_resolve y el texto # crudo se guardaba tal cual como si fuera un string literal. any(op in raw_value for op in ['+', '-', '*', '/', '%', '(', ')', '[']) or ('.' in raw_value and not raw_value.replace('.', '', 1).isdigit()) or ('?' in raw_value and ':' in raw_value) or # ── AGREGADO: comparaciones / lógicos / pertenencia ────────── any(op in raw_value for op in ['==', '!=', '<=', '>=', '<', '>', '&&', '||', '!in']) or re.search(r'\bin\b|\band\b|\bor\b|\bnot\b', raw_value) is not None or # Identificador simple sin comillas = variable, siempre resolver (re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', raw_value.strip()) is not None) ) def handle_VariableDeclaration(self, node): _lc_vd = node.get("linecolumn", {}) if _lc_vd: self._current_line = _lc_vd.get("line") or self._current_line self._current_col = _lc_vd.get("col") or self._current_col name = node["name"] value_node = node.get("value", {}) declared_type = value_node.get("explicitType") or 'dynamic' ast_type = value_node.get("type") raw_value = value_node.get("value") # ── STRUCT INSTANTIATION ───────────────────────────────────────────── # Si el tipo explícito o el tipo AST es una struct conocida, instanciarla effective_type = declared_type if declared_type != 'dynamic' else (ast_type or 'dynamic') if (effective_type and effective_type not in _NATIVE_TYPES and self.struct_table.has_definition(effective_type)): struct_def = self.struct_table.get_definition(effective_type) self._log(f"[STRUCT:NEW] ──────────────────────────────────────") self._log(f"[STRUCT:NEW] Instanciando struct '{effective_type}' → variable '{name}'") instance = StructInstance(effective_type, struct_def, self) new_symbol = Symbol(value=instance, declared_type=effective_type) self.symbol_table.declare(name, new_symbol) self._log(f"[STRUCT:NEW] Instancia #{instance._id} creada y guardada en '{name}'") self._log(f"[STRUCT:NEW] ──────────────────────────────────────") return # ──────────────────────────────────────────────────────────────────── # Nodo de operacion explicita (operation) if "operation" in value_node: op_node = value_node["operation"] expr_s = op_node.get("value") if isinstance(op_node, dict) else op_node initial_value = self.resolve_expression(str(expr_s) if expr_s is not None else '') # NULL explicito elif ast_type == "NULL": initial_value = None # String AST: quitar comillas si las tiene; resolver si tiene operadores elif ast_type == "string" and isinstance(raw_value, str): needs_resolve = ( any(op in raw_value for op in ['+', '-', '*', '/', '%', '(', ')', '[']) or ('.' in raw_value and not raw_value.replace('.', '', 1).isdigit()) or ('?' in raw_value and ':' in raw_value) or # Identificador simple sin comillas = variable, siempre resolver (re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', raw_value.strip()) is not None) or # AGREGADO: operador de comparación/lógico (in, ==, <, and...) # -> siempre resolver como expresión real, nunca guardar el # texto crudo (ver _looks_like_condition_or_comparison). _looks_like_condition_or_comparison(raw_value) ) if needs_resolve: initial_value = self.resolve_expression(raw_value) elif ((raw_value.startswith('"') and raw_value.endswith('"')) or (raw_value.startswith("'") and raw_value.endswith("'"))): initial_value = self._interpolate_hash_string(raw_value[1:-1]) else: initial_value = raw_value # Cualquier otro caso: resolve_expression maneja todo # (new, modulos, OOP, funciones, variables, literales, aritmetica...) else: initial_value = self.resolve_expression(raw_value) # ── Verificar redeclaración en el scope actual ─────────────────────── current_scope = self.symbol_table.symbols[-1] if name in current_scope: raise RedeclarationError( msg("name.redeclaration", name=name), **self._lc_at(node, name) ) # ── Validar tipo explícito contra valor inicial ─────────────────────── # (incluye el AGREGADO de colecciones tipadas y coerción int/float/bool: # ver _validate_declared_type_value) initial_value = self._validate_declared_type_value(declared_type, initial_value, node, raw_value) new_symbol = Symbol(value=initial_value, declared_type=declared_type) self.symbol_table.declare(name, new_symbol) self._log(f"Declarada variable '{name}'. Símbolo: {new_symbol}") # ========================================================================== # ITEM ASSIGNMENT — arr[i] = valor / dict[clave] = valor / arr[i:j] = valor # ========================================================================== # AGREGADO: soporte para reemplazar un elemento existente de un array o un # dict (las tuplas son inmutables y siempre dan error), incluyendo acceso # profundo con varios índices separados por ':' (arr[2:0] = "z"). def _resolve_index_target_value(self, value_node: dict): """ Resuelve el valor a asignar en una asignación indexada (arr[i] = ...), replicando EXACTAMENTE la misma lógica de resolución de valor que handle_VariableAsignement usa para asignaciones simples (nodo de operación explícita, NULL, string con/sin resolución, o cualquier otro caso vía resolve_expression). Se necesita como método aparte (en vez de reutilizar _resolve_assignment_value) porque ese helper no despoja las comillas de los literales string simples; aquí sí lo hacemos, igual que en la asignación de variable normal, para que arr[2:0] = "z"; funcione de forma consistente con x = "z"; """ ast_type = value_node.get("type") raw_value = value_node.get("value") if "operation" in value_node: op_node = value_node["operation"] expr_s = op_node.get("value") if isinstance(op_node, dict) else op_node return self.resolve_expression(str(expr_s) if expr_s is not None else '') if ast_type == "NULL": return None if ast_type == "string" and isinstance(raw_value, str): needs_resolve = ( any(op in raw_value for op in ['+', '-', '*', '/', '%', '(', ')', '[']) or ('.' in raw_value and not raw_value.replace('.', '', 1).isdigit()) or ('?' in raw_value and ':' in raw_value) or (re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', raw_value.strip()) is not None) or # AGREGADO: operador de comparación/lógico -> siempre resolver _looks_like_condition_or_comparison(raw_value) ) if needs_resolve: return self.resolve_expression(raw_value) if ((raw_value.startswith('"') and raw_value.endswith('"')) or (raw_value.startswith("'") and raw_value.endswith("'"))): return self._interpolate_hash_string(raw_value[1:-1]) return raw_value return self.resolve_expression(raw_value) def _resolve_root_value_for_read(self, root_name: str): """ AGREGADO: resuelve el valor ACTUAL de una variable raíz para LECTURA de acceso indexado (arr[i], no arr[i] = ...), soportando tanto variables normales (symbol_table) como PARÁMETROS de función — misma prioridad que ya usa _index_assign_get_root para el lado de escritura. Antes, _evaluate_array_access y el chequeo 'base_known' de resolve_expression sólo probaban symbol_table.get_value(root_name) y fallaban en silencio cuando root_name era un parámetro (ej. 'function ord_burbuja(arreglo){ ... arreglo[j] ... }'). Eso hacía que 'arreglo[j]' NUNCA se reconociera como acceso indexado válido y el texto crudo terminara colándose como si fuera un string literal dentro del array (ej. arreglo[j] = arreglo[j+1]; guardaba la CADENA "arreglo[1]" en vez del número real). Lanza UndeclaredVariableError si no se encuentra en ningún lado (mismo contrato que symbol_table.get_value, para no romper a los llamadores que ya manejan esa excepción). """ # 1. Parámetro de la función actualmente en ejecución current_fn = getattr(self, '_current_function', None) if current_fn: try: fps = self.symbol_table.get_function_params(current_fn) except Exception: fps = {} if root_name in fps: try: return self.symbol_table.get_function_param_value(current_fn, root_name) except UndeclaredVariableError: pass # 2. Tabla de símbolos normal (variables locales/globales) try: return self.symbol_table.get_value(root_name) except UndeclaredVariableError: pass # 3. Cualquier otro frame de función activo (llamadas anidadas/recursivas) for _fn in list(self.symbol_table.function_param_values.keys()): try: return self.symbol_table.get_function_param_value(_fn, root_name) except (UndeclaredVariableError, Exception): pass raise UndeclaredVariableError(root_name) def _index_assign_get_root(self, root_name: str, node: dict, base_expr: str): """ Resuelve el valor ACTUAL de la variable raíz para una asignación indexada (arr[i] = ...), soportando tanto variables normales (guardadas en symbol_table.symbols) como PARÁMETROS de función. IMPORTANTE: en este intérprete los parámetros de función NO viven en symbol_table.symbols — se guardan aparte, en symbol_table.function_param_values, indexados por nombre de función (ver declare_function_param_value / get_function_param_value). Por eso symbol_table.get_value/get_symbol NUNCA encuentran un parámetro, aunque la lectura normal de expresiones (resolve_expression) sí lo resuelve porque primero consulta los parámetros de la función activa. Este helper replica exactamente esa misma prioridad de búsqueda para que 'arreglo[j] = ...' funcione cuando 'arreglo' es un parámetro (como en 'function ord_burbuja(arreglo){ ... }'), y no sólo cuando es una variable local declarada con var/int/etc. Devuelve (root_value, location), donde location es una tupla que indica dónde escribir de vuelta el resultado: ('function_param', frame_key) → parámetro de esa función ('symbol', None) → variable normal en symbol_table """ # 1. Parámetro de la función actualmente en ejecución current_fn = getattr(self, '_current_function', None) if current_fn: try: fps = self.symbol_table.get_function_params(current_fn) except Exception: fps = {} if root_name in fps: try: val = self.symbol_table.get_function_param_value(current_fn, root_name) return val, ('function_param', current_fn) except UndeclaredVariableError: pass # 2. Tabla de símbolos normal (variables locales/globales) try: val = self.symbol_table.get_value(root_name) return val, ('symbol', None) except UndeclaredVariableError: pass # 3. Cualquier otro frame de función activo (llamadas anidadas/recursivas # donde 'root_name' es parámetro de una función distinta a la actual) for _fn in list(self.symbol_table.function_param_values.keys()): try: val = self.symbol_table.get_function_param_value(_fn, root_name) return val, ('function_param', _fn) except (UndeclaredVariableError, Exception): pass raise NameError( msg("name.undefined", name=root_name), **self._lc_at(node, base_expr) ) def _index_assign_set_root(self, root_name: str, location, new_value, node: dict, base_expr: str): """ Persiste el nuevo valor de la variable raíz en el mismo lugar de donde se leyó (ver _index_assign_get_root): parámetro de función o variable normal de symbol_table. """ kind, key = location if kind == 'function_param': self.symbol_table.declare_function_param_value(key, root_name, new_value) return try: self.symbol_table.set_value(root_name, new_value) except (ConstError, NameError) as _e: _lc_err = self._lc_at(node, base_expr) _e.line = _e.line or _lc_err.get("line") _e.col = _e.col or _lc_err.get("col") _e.file = _e.file if _e.file != "" else self._source_file _e.function = _e.function or self._current_function raise def _handle_index_assignment(self, name: str, value_node: dict, node: dict) -> bool: """ Maneja asignaciones a elementos de array o dict (item assignment): arr[2] = 25; → reemplaza el elemento en la posición 2 dic["clave"] = valor; → reemplaza el valor asociado a "clave" arr[2:0] = "z"; → acceso profundo: entra al índice 2 y luego al índice 0 del elemento anidado y reemplaza ahí (ej. arreglo dentro de arreglo) Reglas (según especificación): - La variable base debe existir y ya ser array o dict. - Las tuplas son inmutables: intentar asignar un elemento de una tupla (en cualquier nivel del camino) siempre es error. - Sólo se puede reemplazar un índice/clave que YA EXISTE: no se crean elementos nuevos. arr[10] = 2; sobre un array de longitud menor es error. - El valor asignado puede ser cualquier expresión válida: literal, variable, operación aritmética, comparación (da true/false), u otra colección — incluyendo un array/dict/tupla vacíos, ej. arr[0] = []; - Si la colección es "tipada" (declarada con un tipo escalar, ej. int arr = [1,2,3];), el nuevo valor se valida/coacciona con la misma regla int↔float↔bool que push/insert/.mut (ver _enforce_collection_type_for_var). Devuelve True si 'name' matcheaba el patrón 'algo[índices]' y la asignación fue procesada (o lanzó un error); False si el patrón no aplica y el llamador debe seguir con el flujo normal de asignación. """ m = re.match(r'^([\w.]+)\[(.+)\]$', name.strip(), re.DOTALL) if not m: return False base_expr = m.group(1).strip() raw_index_str = m.group(2).strip() root_name = base_expr.split('.')[0] # ── Resolver la estructura raíz completa ─────────────────────────── # (soporta tanto variables normales como parámetros de función, ver # _index_assign_get_root — antes esto usaba symbol_table.get_value # directo, que NUNCA encuentra un parámetro porque estos se guardan # aparte en function_param_values; eso causaba un NameError falso # en 'arreglo[j] = ...' cuando 'arreglo' era un parámetro) root_value, root_location = self._index_assign_get_root(root_name, node, base_expr) # JSON-string fallback (mismo comportamiento que _evaluate_array_access) if isinstance(root_value, str): stripped = root_value.strip() if (stripped.startswith('[') and stripped.endswith(']')) or \ (stripped.startswith('{') and stripped.endswith('}')): try: root_value = json.loads(stripped.replace("'", '"')) except Exception: raise InvalidOperationError( f"Estructura corrupta en '{root_name}', no se puede asignar por índice.", **self._lc_at(node, base_expr) ) if isinstance(root_value, tuple): raise InvalidOperationError( f"No se puede asignar un elemento de '{root_name}': las tuplas son inmutables.", **self._lc_at(node, base_expr) ) if not isinstance(root_value, (list, dict)): raise InvalidOperationError( f"'{root_name}' no es un array ni un diccionario; no se puede asignar por índice.", **self._lc_at(node, base_expr) ) # Trabajar sobre una copia profunda: si algo falla, el original # queda intacto (ningún cambio a medias). import copy as _copy_mod working_root = _copy_mod.deepcopy(root_value) indices_list = self._split_index_parts(raw_index_str) if not indices_list: raise InvalidOperationError( f"Índice vacío o inválido en '{base_expr}[{raw_index_str}]'.", **self._lc_at(node, base_expr) ) # ── Navegar hasta el contenedor que posee el ÚLTIMO índice ───────── current = working_root for level, raw_idx in enumerate(indices_list[:-1]): idx_val = self._resolve_index_value(raw_idx.strip()) if isinstance(current, tuple): raise InvalidOperationError( f"No se puede asignar dentro de una tupla anidada en " f"'{base_expr}[{raw_index_str}]': las tuplas son inmutables.", **self._lc_at(node, base_expr) ) if not isinstance(current, (list, dict)): raise DeepNotCollectionError( msg("deep.not_collection", depth=level, got=type(current).__name__), depth=level, got_type=type(current).__name__, path=indices_list, **self._lc_at(node, base_expr) ) if isinstance(current, dict): if idx_val in current: current = current[idx_val] elif isinstance(idx_val, int): keys = list(current.keys()) if 0 <= idx_val < len(keys): current = current[keys[idx_val]] else: raise DeepIndexOutOfRangeError( msg("deep.out_of_range", depth=level, index=idx_val, length=len(keys)), depth=level, index=idx_val, length=len(keys), path=indices_list, **self._lc_at(node, base_expr) ) else: raise DeepKeyError( msg("deep.key_not_found", depth=level, key=idx_val), depth=level, key=idx_val, path=indices_list, **self._lc_at(node, base_expr) ) else: # list if not isinstance(idx_val, int): try: idx_val = int(idx_val) except (TypeError, ValueError): raise DeepTypeMismatchError( msg("deep.type_mismatch", depth=level, got=type(idx_val).__name__, expected="int"), depth=level, expected="int", got=type(idx_val).__name__, path=indices_list, **self._lc_at(node, base_expr) ) length = len(current) if -length <= idx_val < length: current = current[idx_val] else: raise DeepIndexOutOfRangeError( msg("deep.out_of_range", depth=level, index=idx_val, length=length), depth=level, index=idx_val, length=length, path=indices_list, **self._lc_at(node, base_expr) ) # ── Nivel final: aquí ocurre el reemplazo real ───────────────────── last_raw = indices_list[-1].strip() last_idx = self._resolve_index_value(last_raw) last_level = len(indices_list) - 1 if isinstance(current, tuple): raise InvalidOperationError( f"No se puede asignar un elemento de una tupla en " f"'{base_expr}[{raw_index_str}]': las tuplas son inmutables.", **self._lc_at(node, base_expr) ) if not isinstance(current, (list, dict)): raise DeepNotCollectionError( msg("deep.not_collection", depth=last_level, got=type(current).__name__), depth=last_level, got_type=type(current).__name__, path=indices_list, **self._lc_at(node, base_expr) ) # Resolver el valor a asignar: soporta literales, variables, # aritmética, comparaciones (true/false) y colecciones (incluyendo # arrays/dicts/tuplas vacíos como arr[0] = [];). final_value = self._resolve_index_target_value(value_node) if isinstance(current, dict): if last_idx in current: current[last_idx] = final_value elif isinstance(last_idx, int): keys = list(current.keys()) if 0 <= last_idx < len(keys): current[keys[last_idx]] = final_value else: raise DeepIndexOutOfRangeError( msg("deep.out_of_range", depth=last_level, index=last_idx, length=len(keys)), depth=last_level, index=last_idx, length=len(keys), path=indices_list, **self._lc_at(node, base_expr) ) else: raise DeepKeyError( msg("deep.key_not_found", depth=last_level, key=last_idx), depth=last_level, key=last_idx, path=indices_list, **self._lc_at(node, base_expr) ) else: # list if not isinstance(last_idx, int): try: last_idx = int(last_idx) except (TypeError, ValueError): raise DeepTypeMismatchError( msg("deep.type_mismatch", depth=last_level, got=type(last_idx).__name__, expected="int"), depth=last_level, expected="int", got=type(last_idx).__name__, path=indices_list, **self._lc_at(node, base_expr) ) length = len(current) if -length <= last_idx < length: current[last_idx] = final_value else: raise DeepIndexOutOfRangeError( msg("deep.out_of_range", depth=last_level, index=last_idx, length=length), depth=last_level, index=last_idx, length=length, path=indices_list, **self._lc_at(node, base_expr) ) # ── Validar/coaccionar tipo de colección tipada antes de persistir ── # (mismo mecanismo que push/insert/.mut: sólo actúa si la variable # raíz fue declarada con un tipo escalar, ver # _enforce_collection_type_for_var; si es un parámetro, esta función # ya no-opea de forma segura porque no encuentra símbolo declarado). working_root = self._enforce_collection_type_for_var(root_name, working_root) # Persistir en el mismo lugar de donde se leyó (parámetro de función # o variable normal), ver _index_assign_set_root. self._index_assign_set_root(root_name, root_location, working_root, node, base_expr) self._log(f"[INDEX-ASSIGN] {base_expr}[{raw_index_str}] = {final_value}") return True def _get_param_type_name(self, value): """ AGREGADO/REPARADO: usada para validar el tipo de un parámetro cuando se le REASIGNA un valor dentro del cuerpo de la función (ver handle_VariableAsignement). Delega a _get_value_type (la misma función canónica que ahora usa también FunctionContext._get_type_name para la validación en la LLAMADA), para que un parámetro tipado como 'array'/'tuple'/'dict' no caiga en 'any' al reasignarle una colección — antes esta réplica sólo reconocía bool/int/float/string. """ return _get_value_type(value) def handle_VariableAsignement(self, node): _lc_va = node.get("linecolumn", {}) if _lc_va: self._current_line = _lc_va.get("line") or self._current_line self._current_col = _lc_va.get("col") or self._current_col name = node["name"] value_node = node.get("value", {}) ast_type = value_node.get("type") raw_value = value_node.get("value") # Asignacion a elemento de array/dict: arr[i] = valor; arr[i:j] = valor; # (las tuplas dan error: ver _handle_index_assignment) if isinstance(name, str) and re.match(r'^[\w.]+\[.+\]$', name.strip(), re.DOTALL): if self._handle_index_assignment(name, value_node, node): return # Asignacion de atributo OOP: obj.campo = valor if isinstance(name, str) and '.' in name: parts = name.split('.', 1) obj_name, field = parts try: obj = self.symbol_table.get_value(obj_name) if isinstance(obj, ClassInstance): final_value = self._resolve_assignment_value(value_node) obj.set_attribute(field, final_value) self._log(f"[OOP] {obj_name}.{field} = {final_value}") return except TesseractError: raise except UndeclaredVariableError: pass # Nodo de operacion explicita if "operation" in value_node: op_node = value_node["operation"] expr_s = op_node.get("value") if isinstance(op_node, dict) else op_node final_value = self.resolve_expression(str(expr_s) if expr_s is not None else '') # NULL explicito elif ast_type == "NULL": final_value = None # String AST elif ast_type == "string" and isinstance(raw_value, str): needs_resolve = ( any(op in raw_value for op in ['+', '-', '*', '/', '%', '(', ')', '[']) or ('.' in raw_value and not raw_value.replace('.', '', 1).isdigit()) or ('?' in raw_value and ':' in raw_value) or # Identificador simple sin comillas = variable, siempre resolver (re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', raw_value.strip()) is not None) or # AGREGADO: operador de comparación/lógico (in, ==, <, and...) # -> siempre resolver como expresión real, nunca guardar el # texto crudo (ver _looks_like_condition_or_comparison). _looks_like_condition_or_comparison(raw_value) ) if needs_resolve: final_value = self.resolve_expression(raw_value) elif ((raw_value.startswith('"') and raw_value.endswith('"')) or (raw_value.startswith("'") and raw_value.endswith("'"))): final_value = self._interpolate_hash_string(raw_value[1:-1]) else: final_value = raw_value # Cualquier otro caso else: final_value = self.resolve_expression(raw_value) # ══════════════════════════════════════════════════════════════════ # AGREGADO: subnodo "assing" — asignación compuesta estilo C # ══════════════════════════════════════════════════════════════════ # Este subnodo es OPCIONAL — no siempre viene en el AST. # - Si no está: todo sigue exactamente igual (asignación normal). # - Si está y es "=" (o cualquier otra cosa no reconocida): también # es asignación normal, sin cambios. # - Si está y es "+=", "-=", "*=", "/=" o "%=": el valor final se # recalcula como (valor ACTUAL de la variable) OP (valor ya # resuelto de la derecha), exactamente igual que en C # (x += y equivale a x = x + y, etc.). # Sólo aplica a esta asignación de variable simple (no a los casos # de arriba que ya retornaron antes: asignación a índice de # array/dict, o a atributo OOP con punto). _assing_node = node.get("assing") _assing_op = (_assing_node.get("value") if isinstance(_assing_node, dict) else _assing_node) _COMPOUND_ASSIGN_OPS = {'+=': '+', '-=': '-', '*=': '*', '/=': '/', '%=': '%'} if _assing_op in _COMPOUND_ASSIGN_OPS: try: _current_value = self.symbol_table.get_value(name) except UndeclaredVariableError: raise NameError( msg("name.undefined", name=name), **self._lc_at(node, name) ) try: final_value = self._me_arith( _COMPOUND_ASSIGN_OPS[_assing_op], _current_value, final_value ) except _MEDivZero: raise DivisionByZeroError( msg("op.division_zero"), **self._lc_at(node, name) ) except _METypeError as _mt: raise InvalidOperandError( msg("op.invalid_operand", got=_mt.message, op=_assing_op), **self._lc_at(node, name) ) self._log(f"[asignación compuesta] '{name}' {_assing_op} -> {final_value!r}") # ══════════════════════════════════════════════════════════════════ # AGREGADO: asignación a un PARÁMETRO de la función actual, cuando # el AST no emitió un nodo "parameterAsignement" dedicado (ver # handle_ParameterAsignement, que se deja intacto) y esta asignación # llegó aquí como una "variableAsignement" genérica. # # Replica la misma prioridad que YA usa el resto del intérprete para # LECTURAS (resolve_expression paso 12, evaluate_expression, # _evaluate_arithmetic_operation, etc.): dentro de una función, un # nombre simple se busca PRIMERO en la tabla de parámetros de esa # función y sólo si no está ahí se trata como variable normal # (símbolo local/global en symbol_table) — igual que en Python, el # parámetro es local y tiene prioridad sobre cualquier variable # externa con el mismo nombre. # # Se valida el tipo declarado del parámetro (si tiene uno explícito # distinto de any/dynamic) con la MISMA regla usada al recibir el # argumento en la llamada (promoción int → float permitida; # cualquier otro choque de tipo/forma es un ArgumentTypeError), para # no perder las validaciones de tipado de los parámetros. if (isinstance(name, str) and hasattr(self, '_current_function') and self._current_function): _fn_params = self.symbol_table.get_function_params(self._current_function) if name in _fn_params: _expected_type = _fn_params[name] if _expected_type not in ('any', 'dynamic', '', None): _actual_type = self._get_param_type_name(final_value) if _expected_type == 'float' and _actual_type == 'int': pass # promoción permitida, igual que en la llamada elif _actual_type != _expected_type: raise ArgumentTypeError( msg("func.arg_type", param=name, func=self._current_function, expected=_expected_type, got=_actual_type), **self._lc_at(node, name) ) self.symbol_table.declare_function_param_value( self._current_function, name, final_value ) self._log(f"[PARAM-ASSIGN] Asignado parámetro '{name}' = {final_value!r}") return # ── Validar tipo declarado del símbolo existente contra el nuevo valor ── # (Mismo chequeo que ya existe en VariableDeclaration, pero faltaba aquí # para la reasignación: int x = 5; ... x = "hola"; debe fallar.) # AGREGADO: incluye colecciones tipadas y coerción int/float/bool # (ver _validate_declared_type_value). try: existing_symbol = self.symbol_table.get_symbol(name) except UndeclaredVariableError: existing_symbol = None if existing_symbol is not None: final_value = self._validate_declared_type_value( existing_symbol.declared_type, final_value, node, raw_value ) try: self.symbol_table.set_value(name, final_value) except (ConstError, NameError) as _e: # Enriquecer con ubicación del nodo de asignación, apuntando # exactamente al nombre de la variable en esta línea. _lc_err = self._lc_at(node, name) _e.line = _e.line or _lc_err.get("line") _e.col = _e.col or _lc_err.get("col") _e.file = _e.file if _e.file != "" else self._source_file _e.function = _e.function or self._current_function raise self._log(f"Asignado nuevo valor a '{name}': {final_value}") # ========================================================================== # ¡CORRECCIÓN 2: Impresión inteligente de variables! # ========================================================================== def _resolve_assignment_value(self, value_node: dict): """Resuelve el valor de cualquier asignación (helper compartido).""" if "operation" in value_node: op = value_node["operation"] expr = op.get("value") if isinstance(op, dict) else op return self.resolve_expression(expr) raw = value_node.get("value") atype = value_node.get("type") if isinstance(raw, str) and self._is_function_call(raw): return self._execute_function_call_from_string(raw) if isinstance(raw, str) and not (raw.startswith('"') and raw.endswith('"')): return self.resolve_expression(raw) if isinstance(raw, str) and raw.startswith('"') and raw.endswith('"') and '#(' in raw: # Preserva el comportamiento previo (devuelve con comillas), solo # resuelve el formateo #(...) que hubiera dentro del literal. return '"' + self._interpolate_hash_string(raw[1:-1]) + '"' return raw def handle_ConstantDeclaration(self, node): name = node["name"] value_node = node.get("value", {}) declared_type = value_node.get("explicitType") or 'dynamic' ast_type = value_node.get("type") raw_value = value_node.get("value") # Nodo de operacion explicita if "operation" in value_node: op_node = value_node["operation"] expr_s = op_node.get("value") if isinstance(op_node, dict) else op_node initial_value = self.resolve_expression(str(expr_s) if expr_s is not None else '') # NULL explicito elif ast_type == "NULL": initial_value = None # String AST elif ast_type == "string" and isinstance(raw_value, str): needs_resolve = ( any(op in raw_value for op in ['+', '-', '*', '/', '%', '(', ')', '[']) or ('.' in raw_value and not raw_value.replace('.', '', 1).isdigit()) or # AGREGADO: operador de comparación/lógico -> siempre resolver _looks_like_condition_or_comparison(raw_value) ) if needs_resolve: initial_value = self.resolve_expression(raw_value) elif ((raw_value.startswith('"') and raw_value.endswith('"')) or (raw_value.startswith("'") and raw_value.endswith("'"))): initial_value = self._interpolate_hash_string(raw_value[1:-1]) else: initial_value = raw_value # Cualquier otro caso else: initial_value = self.resolve_expression(raw_value) new_symbol = Symbol(value=initial_value, declared_type=declared_type, is_const=True) self.symbol_table.declare(name, new_symbol) self._log(f"Declarada constante '{name}'. Símbolo: {new_symbol}") # ========================================================================== # STRUCT — Declaración y Asignación de Módulo # ========================================================================== def handle_StructDeclaration(self, node): """Registra un struct en la tabla de structs.""" struct_name = node["name"] block = node.get("block", []) struct_def = StructDefinition(struct_name) for member in block: if not isinstance(member, dict): continue node_type = list(member.keys())[0] content = member[node_type] is_const = node_type == "ConstantDeclarationStruct" field_name = content["name"] value_node = content.get("value", {}) # Tipo declarado (explícito o inferido del AST) declared_type = (value_node.get("explicitType") or value_node.get("type") or "dynamic") if declared_type in ("NULL", "null"): declared_type = "dynamic" declared_type = declared_type.lower() if isinstance(declared_type, str) else "dynamic" # Límite de colección [N] — puede estar en content o dentro de value_node limit_node = content.get("limit") or value_node.get("limit") limit = (limit_node.get("value") if isinstance(limit_node, dict) else limit_node) # Es dinámica (sin tipo explícito fijo) din_node = value_node.get("dinamic") is_dynamic = (din_node.get("value") if isinstance(din_node, dict) else True) if is_dynamic is None: is_dynamic = True # Valor inicial raw_v = value_node.get("value") ast_type = value_node.get("type", "") if ast_type in ("NULL", "null") or raw_v in (None, "null"): initial_value = None elif raw_v == "[]": initial_value = [] elif raw_v == "{}": initial_value = {} elif isinstance(raw_v, bool): initial_value = raw_v elif isinstance(raw_v, (int, float)): initial_value = raw_v elif isinstance(raw_v, str) and raw_v.startswith('[') and raw_v.endswith(']'): try: initial_value = self.resolve_expression(raw_v) except Exception: initial_value = [] else: initial_value = None fdef = StructFieldDef( name = field_name, declared_type = declared_type, is_const = is_const, initial_value = initial_value, limit = limit, is_dynamic = is_dynamic, ) struct_def.add_field(fdef) self.struct_table.declare(struct_name, struct_def) field_count = len(struct_def.fields) self._log(f"[STRUCT:DEF] ══════════════════════════════════════") self._log(f"[STRUCT:DEF] Struct '{struct_name}' registrada ({field_count} campo(s)):") for fname, fdef in struct_def.fields.items(): mod = "const " if fdef.is_const else "" lim = f"[{fdef.limit}]" if fdef.limit is not None else "" dyn = " (dinámico)" if fdef.is_dynamic else "" self._log(f"[STRUCT:DEF] {mod}{fdef.declared_type}{lim} {fname}" f" = {fdef.initial_value!r}{dyn}") self._log(f"[STRUCT:DEF] ══════════════════════════════════════") def handle_ModuleAsignement(self, node): """ Maneja asignaciones con punto: p1.name = "Ana", p1.dir.calle = "Madrid". Aplica las reglas de tipado y límite de cada campo de la struct. """ full_name = node["name"] # e.g. "p1.name" o "p1.dir.calle" value_node = node.get("value", {}) longitud_node = node.get("longitud") parts = full_name.split(".") root_name = parts[0] field_path = parts[1:] # ["name"] o ["dir", "calle"] if not field_path: raise InterpreterError( f"ModuleAsignement: '{full_name}' no contiene un campo de acceso.", **self._lc_at(node, root_name) ) # ── Resolver el valor a asignar ─────────────────────────────────────── ast_type = value_node.get("type", "") raw_value = value_node.get("value", "") final_value = self._resolve_struct_assignment_value(value_node, ast_type, raw_value) # ── Obtener la variable raíz ───────────────────────────────────────── try: root_val = self.symbol_table.get_value(root_name) except UndeclaredVariableError: raise InterpreterError( f"[Struct] Variable '{root_name}' no declarada.", **self._lc_at(node, root_name) ) if not isinstance(root_val, StructInstance): # Fallback: podría ser una clase OOP u otro objeto # Delegar al manejador OOP existente try: sym = self.symbol_table.get_symbol(root_name) if isinstance(sym.value, ClassInstance): self._oop_set_field(sym.value, field_path, final_value) return except Exception: pass raise InterpreterError( f"[Struct] '{root_name}' no es una instancia de struct " f"(tipo: {type(root_val).__name__}).", **self._lc_at(node, root_name) ) # ── Navegar structs anidados hasta el penúltimo campo ──────────────── instance = root_val for field in field_path[:-1]: try: nested = instance.get_field(field) except StructFieldError as _e: _lc_err = self._lc_at(node, field) _e.line = _e.line or _lc_err.get("line") _e.col = _e.col or _lc_err.get("col") _e.file = _e.file if _e.file != "" else self._source_file _e.function = _e.function or self._current_function raise if not isinstance(nested, StructInstance): raise InterpreterError( f"[Struct] '{field}' no es una struct anidada; " f"no se puede navegar más profundo.", **self._lc_at(node, field) ) instance = nested target_field = field_path[-1] fdef = instance.struct_def.fields.get(target_field) if fdef is None: raise InterpreterError( f"[Struct] Campo '{target_field}' no existe en struct " f"'{instance.struct_name}'.", **self._lc_at(node, target_field) ) # ── Validar tipo ───────────────────────────────────────────────────── self._validate_struct_field_value(final_value, fdef, target_field, instance.struct_name, raw_text=str(raw_value) if raw_value is not None else None) # ── Validar límite contra longitud reportada por el AST ────────────── if longitud_node is not None and isinstance(final_value, (list, tuple, dict)): ast_len = (longitud_node.get("value") if isinstance(longitud_node, dict) else longitud_node) if fdef.limit is not None and ast_len > fdef.limit: raise InvalidOperationError( f"[Struct] Campo '{target_field}' tiene límite {fdef.limit}, " f"se intentó asignar {ast_len} elementos.", **self._lc_here(str(raw_value) if raw_value is not None else target_field) ) # ── Asignar ────────────────────────────────────────────────────────── old_val = instance.fields[target_field]['value'] try: instance.set_field(target_field, final_value, fdef) except (StructFieldError, StructFieldConstError) as _e: _lc_err = self._lc_at(node, target_field) _e.line = _e.line or _lc_err.get("line") _e.col = _e.col or _lc_err.get("col") _e.file = _e.file if _e.file != "" else self._source_file _e.function = _e.function or self._current_function raise const_tag = " [const → bloqueado para reasignación]" if fdef.is_const else "" self._log( f"[STRUCT:SET] {full_name} :: {fdef.declared_type}" f"{'['+str(fdef.limit)+']' if fdef.limit else ''}" f" | {old_val!r} → {final_value!r}{const_tag}") def _resolve_struct_assignment_value(self, value_node, ast_type, raw_value): """ Resuelve el valor a asignar a un campo de struct. Sigue las mismas reglas que handle_VariableDeclaration: - strings → desquota "Ana" → Ana - arrays → resuelve el literal - tuples → parsea la tupla - dicts → parsea el dict JSON - otros → resolve_expression general """ ast_type_lower = ast_type.lower() if isinstance(ast_type, str) else "" # Operación explícita if "operation" in value_node: op = value_node["operation"] expr = op.get("value") if isinstance(op, dict) else op return self.resolve_expression(str(expr) if expr is not None else '') # NULL if ast_type in ("NULL", "null") or raw_value in (None, "null"): return None # String → desquotar igual que VariableDeclaration if ast_type_lower == "string" and isinstance(raw_value, str): needs_resolve = ( any(op in raw_value for op in ['+', '%', '(', ')', '[']) or ('.' in raw_value and not raw_value.replace('.', '', 1).isdigit()) or # AGREGADO: operador de comparación/lógico -> siempre resolver _looks_like_condition_or_comparison(raw_value) ) if needs_resolve: return self.resolve_expression(raw_value) if ((raw_value.startswith('"') and raw_value.endswith('"')) or (raw_value.startswith("'") and raw_value.endswith("'"))): return self._interpolate_hash_string(raw_value[1:-1]) return raw_value # Tuple literal: "(val1, val2)" if ast_type_lower == "tuple" and isinstance(raw_value, str): return self._parse_tuple_literal(raw_value) # Dict literal: '{"k":"v"}' if ast_type_lower == "dict" and isinstance(raw_value, str): try: import ujson as _uj return _uj.loads(raw_value) except Exception: return {} # Array literal: "[1,2,3]" if ast_type_lower == "array" and isinstance(raw_value, str): try: return self.resolve_expression(raw_value) except Exception: return [] # Bool, int, float nativos de Python if isinstance(raw_value, bool): return raw_value if isinstance(raw_value, int): return raw_value if isinstance(raw_value, float): return raw_value # Caso general if isinstance(raw_value, str): try: return self.resolve_expression(raw_value) except Exception: return raw_value return raw_value def _parse_tuple_literal(self, raw: str) -> tuple: """Parsea '("stat1", "stat2")' → Python tuple.""" raw = raw.strip() if raw.startswith('(') and raw.endswith(')'): inner = raw[1:-1].strip() if not inner: return tuple() items = self._split_args_respecting_brackets(inner) return tuple(self.resolve_expression(it.strip()) for it in items) raise InterpreterError(f"[Struct] No es una tupla válida: '{raw}'", **self._lc()) def _validate_struct_field_value(self, value, fdef: 'StructFieldDef', field_name: str, struct_name: str, raw_text: str = None): """Valida que `value` sea compatible con el tipo declarado del campo. raw_text: texto literal del valor tal como aparece en el código (ej. '"hola"', '42'), usado para anclar el cursor '^^^' del traceback exactamente al valor que causó el conflicto, en vez de a la línea completa de la asignación. """ dtype = fdef.declared_type limit = fdef.limit if dtype in ('dynamic', None, 'null'): return # sin restricción de tipo def _cat(v): if v is None: return 'null' if isinstance(v, RangeValue): return 'range' if isinstance(v, bool): return 'bool' if isinstance(v, int): return 'int' if isinstance(v, float): return 'float' if isinstance(v, str): return 'string' if isinstance(v, tuple): return 'tuple' if isinstance(v, list): return 'array' if isinstance(v, dict): return 'dict' return 'unknown' actual = _cat(value) self._log( f"[STRUCT:TYPECHECK] campo '{field_name}' en '{struct_name}': " f"esperado={dtype}" f"{'['+str(limit)+']' if limit else ''}, recibido={actual}" f"{'(len='+str(len(value))+')' if isinstance(value, (list,tuple,dict)) else ''}") def _type_error(expected): raise StructFieldTypeError( msg("struct.field_type_mismatch", field=field_name, struct=struct_name, expected=expected, got=actual), **self._lc_here(raw_text) ) def _limit_error(got): raise CollectionLimitError( msg("collection.limit_exceeded", name=f"{struct_name}.{field_name}", limit=limit), **self._lc_here(raw_text) ) # Rango tipado if dtype == 'range': if actual != 'range': _type_error('range') return if dtype == 'array': if actual != 'array': _type_error('array') if limit is not None and len(value) > limit: _limit_error(len(value)) elif dtype == 'tuple': if actual != 'tuple': _type_error('tuple') if limit is not None and len(value) > limit: _limit_error(len(value)) elif dtype == 'dict': if actual != 'dict': _type_error('dict') if limit is not None and len(value) > limit: _limit_error(len(value)) elif dtype in ('int', 'float', 'string', 'bool'): if limit is not None: # Tipo primitivo con límite → espera una colección de ese tipo if actual not in ('array', 'tuple', 'dict'): _type_error(f"{dtype}[{limit}] (colección)") if len(value) > limit: _limit_error(len(value)) # Validar elementos individuales for elem in (value.values() if isinstance(value, dict) else value): ea = _cat(elem) if ea != dtype: raise InvalidOperationError( f"[Struct] El campo '{field_name}' espera elementos de tipo " f"'{dtype}', pero contiene '{ea}'.", **self._lc_here(raw_text) ) else: if actual != dtype: # Permitir promoción int→float if not (dtype == 'float' and actual == 'int'): _type_error(dtype) # Tipo struct anidado: ya gestionado en _init_fields; se ignora aquí def _oop_set_field(self, instance: 'ClassInstance', field_path: list, value): """Helper: asigna un campo en una ClassInstance vía lista de partes.""" obj = instance for part in field_path[:-1]: obj = obj.get_attribute(part) obj.set_attribute(field_path[-1], value) def _get_struct_field_by_path(self, instance: 'StructInstance', path: str): """ Navega un path de puntos a través de StructInstances anidadas. Cuando se agota la cadena de structs, el resto se delega a _execute_type_method_chain para compatibilidad con la interfaz de tipos. Ejemplos: "name" → instance.name (valor final) "dir.calle" → instance.dir.calle "name.type" → type-method 'type' sobre el valor de name "name.length()" → type-method 'length' sobre el valor de name "name.slice(0,3)" → type-method 'slice' con args sobre name "dir.calle.toUpperCase()" → type-method sobre campo anidado "hobbies.contains(\"dev\")" → type-method array sobre campo anidado """ # Split por puntos respetando paréntesis parts = self._split_dot_path_smart(path) current = instance resolved_parts = [] # partes ya navegadas como structs for i, part in enumerate(parts): if isinstance(current, StructInstance): # nombre del campo sin args (slice(0,3) → slice) field_name = re.match(r'^([A-Za-z_]\w*)', part) field_name = field_name.group(1) if field_name else part # ¿Es un campo del struct actual? if field_name in current.struct_def.fields: resolved_parts.append(field_name) self._log( f"[STRUCT:GET] navegar '{field_name}' en struct " f"'{current.struct_name}' → " f"{current.fields[field_name]['value']!r}") current = current.get_field(field_name) else: # No es campo → debe ser type-method sobre el struct mismo remaining_chain = self._join_dot_parts(parts[i:]) val_type = _get_value_type(current) first_m = field_name if first_m in _CORE_TYPE_METHODS.get(val_type, {}): self._log( f"[STRUCT:GET] '{part}' no es campo de struct " f"'{current.struct_name}'; delegando type-method " f"'{remaining_chain}' sobre valor tipo '{val_type}'") result, _ = self._execute_type_method_chain( ".".join(resolved_parts), remaining_chain, current, False) return result raise StructFieldError( msg("struct.field_missing", field=field_name, struct=current.struct_name), **self._lc() ) else: # Ya salimos de los structs — el resto son type-methods remaining_chain = self._join_dot_parts(parts[i:]) val_type = _get_value_type(current) first_m = re.match(r'^([A-Za-z_]\w*)', part) first_m = first_m.group(1) if first_m else part if val_type in _CORE_TYPE_METHODS and \ first_m in _CORE_TYPE_METHODS[val_type]: self._log( f"[STRUCT:GET] aplicando type-method '{remaining_chain}' " f"sobre valor tipo '{val_type}' = {current!r}") result, _ = self._execute_type_method_chain( ".".join(resolved_parts), remaining_chain, current, False) return result raise InterpreterError( f"[Struct] No se puede acceder a '{part}' " f"en un valor de tipo '{val_type}'; " f"no es un campo de struct ni un método del tipo.", **self._lc()) return current def _split_dot_path_smart(self, path: str) -> list: """ Divide 'a.b.slice(0,3).c' por puntos sin romper dentro de paréntesis. Resultado: ['a', 'b', 'slice(0,3)', 'c'] """ parts = [] depth = 0 buf = [] for ch in path: if ch in ('(', '[', '{'): depth += 1 buf.append(ch) elif ch in (')', ']', '}'): depth -= 1 buf.append(ch) elif ch == '.' and depth == 0: if buf: parts.append(''.join(buf)) buf = [] else: buf.append(ch) if buf: parts.append(''.join(buf)) return parts def _join_dot_parts(self, parts: list) -> str: """Reconstruye una cadena de dot-parts de forma segura.""" return ".".join(parts) # =========================================================================== # RANGE HELPERS # =========================================================================== def _try_parse_range(self, expr: str): """ Intenta parsear expr como range literal, soportando: 1..5 → cerrado 1..<5 → semi-abierto (excluye end) "a".."z" → string (unicode en comillas: "u0041".."u007f") null..null → rango nulo Retorna RangeValue o None. Lanza InterpreterError si es inválido. """ expr = expr.strip() if '..' not in expr: return None parts, half_open = self._split_by_dotdot(expr) if len(parts) != 2: return None raw_s = self._resolve_range_endpoint_var(parts[0].strip()) raw_e = self._resolve_range_endpoint_var(parts[1].strip()) if raw_s is None or raw_e is None: return None try: return _build_range(raw_s, raw_e, half_open=half_open) except ValueError as e: err_str = str(e) if "inicio" in err_str or "mayor" in err_str or "start" in err_str: raise RangeBoundsError( msg("range.bounds", start=raw_s, end=raw_e), **self._lc_here(expr) ) if "incompatible" in err_str or "tipos" in err_str: raise RangeTypeError( msg("range.type_incompatible", left=raw_s, right=raw_e), **self._lc_here(expr) ) if "decimal" in err_str: raise RangeFloatPrecisionError( err_str, **self._lc_here(expr) ) if "unicode" in err_str or "unicode" in err_str.lower(): raise RangeUnicodeError( err_str, **self._lc_here(expr) ) raise RangeError( f"Rango inválido '{expr}': {e}", **self._lc_here(expr) ) except Exception: return None def _find_matching_close(self, s: str, open_pos: int) -> int: """ Dado que s[open_pos] == '(', devuelve el índice del ')' que lo cierra. Respeta strings anidados y paréntesis anidados. Retorna None si no encontrado. """ depth = 0; in_str = False; str_ch = '"' for i in range(open_pos, len(s)): ch = s[i] if in_str: if ch == str_ch: in_str = False continue if ch in ('"', "'"): in_str = True; str_ch = ch; continue if ch == '(': depth += 1 elif ch == ')': depth -= 1 if depth == 0: return i return None def _split_by_dotdot(self, expr: str): """ Divide por '..' o '..<' respetando paréntesis/corchetes/llaves y strings. Solo divide cuando la profundidad de brackets es 0. '1..5' → (['1','5'], False) '1..<5' → (['1','5'], True) '(0..x.length)..5' → (['(0..x.length)','5'], False) ← no parte dentro de () """ i = 0; parts = []; buf = []; in_str = False; str_ch = '"'; depth = 0 while i < len(expr): ch = expr[i] if in_str: buf.append(ch) if ch == str_ch: in_str = False i += 1; continue if ch in ('"', "'"): in_str = True; str_ch = ch; buf.append(ch); i += 1; continue if ch in ('(', '[', '{'): depth += 1; buf.append(ch); i += 1; continue if ch in (')', ']', '}'): depth -= 1; buf.append(ch); i += 1; continue # Solo partir cuando depth == 0 if depth == 0 and ch == '.' and i + 2 < len(expr) \ and expr[i+1] == '.' and expr[i+2] == '<': parts.append(''.join(buf)); buf = [] i += 3 parts.append(expr[i:].strip()) return parts, True if depth == 0 and ch == '.' and i + 1 < len(expr) and expr[i+1] == '.': parts.append(''.join(buf)); buf = []; i += 2; continue buf.append(ch); i += 1 parts.append(''.join(buf)) return parts, False def _resolve_range_endpoint_var(self, raw: str): """ Resuelve un endpoint de rango a su representación en texto. Acepta: literales, variables simples, y expresiones complejas (x.length, arr[0], etc.) Retorna None si el resultado no es usable como endpoint de rango. """ raw = raw.strip() # Literales directos — devolver tal cual sin evaluar if (raw.startswith('"') and raw.endswith('"')) or \ (raw.startswith("'") and raw.endswith("'")): return raw if raw == 'null': return raw if re.fullmatch(r'-?\d+\.\d+|-?\d+', raw): return raw # Cualquier otra cosa → resolver como expresión try: val = self.resolve_expression(raw) if val is None: return 'null' if isinstance(val, bool): return None # bool no válido en rango if isinstance(val, int): return str(val) if isinstance(val, float): return str(val) if isinstance(val, str): # Carácter único → endpoint string válido if len(val) == 1: return f'"{val}"' return None # string de >1 char no es endpoint válido except Exception: return None return None def _builtin_tesseract_config(self): """ Implementa la función nativa __tesseract_config() del lenguaje Tesseract. Devuelve un dict Python con la configuración .tsc activa (la fusión de la configuración global + local + overrides CLI que se cargó antes de iniciar la interpretación). Si no hay .tsc activo, devuelve la configuración de modo libre. En el lenguaje el usuario lo llama así: var config = __tesseract_config(); if (config["strict-mode:typing"] == "only-static") { ... } """ if self._tsc_config: return dict(self._tsc_config) # Sin .tsc cargado → modo libre por defecto if _tsc_engine is not None: return dict(_tsc_engine.DEFAULT_TSC) return { "strict-mode": { "enabled": False, "typing": "free", "only-block": None, "entry_point-strict": False, "execution_mode": "free", }, "transform-code": { "enabled": False, "mutable": False, "target": "brace", }, "allow-cli-override": True, } def _builtin_tesseract_capabilities(self): """ Implementa la función nativa __tesseract_capabilities() del lenguaje. Devuelve un dict con las capacidades del entorno actual, derivado de la configuración .tsc activa. Útil para código que adapta su comportamiento al entorno de ejecución sin romper la promesa de 'las capacidades del lenguaje no cambian'. En el lenguaje el usuario lo llama así: var cap = __tesseract_capabilities(); if (cap["strict_mode"] == true) { ... } """ if _tsc_engine is not None: cfg = self._tsc_config if self._tsc_config else _tsc_engine.DEFAULT_TSC return _tsc_engine.get_capabilities(cfg) # Fallback sin tsc_engine: modo libre cfg = self._tsc_config if self._tsc_config else {} sm = cfg.get("strict-mode", {}) tc = cfg.get("transform-code", {}) return { "strict_mode": sm.get("enabled", False), "typing": sm.get("typing", "free"), "block_style": sm.get("only-block"), "entry_point_strict": sm.get("entry_point-strict", False), "execution_mode": sm.get("execution_mode", "free"), "transform_enabled": tc.get("enabled", False), "transform_mutable": tc.get("mutable", False), "transform_target": tc.get("target", "brace"), "cli_override": cfg.get("allow-cli-override", True), } def handle_CallExpression(self, node): # Actualizar línea/col con el nodo de esta llamada _lc_call = node.get("linecolumn", {}) if _lc_call: self._current_line = _lc_call.get("line") or self._current_line self._current_col = _lc_call.get("col") or self._current_col function_name = node.get("function") if isinstance(function_name, str) and '.' in function_name: clean = function_name.strip().rstrip('()') parts = clean.split('.', 1) if len(parts) == 2: left, right = parts # --- comprobación de instancias de clase (existente) --- # --- comprobación de módulos (existente) --- # ===== NUEVO: métodos de tipos primitivos (array, string, etc.) ===== # AGREGADO: antes se envolvía todo (incluida la llamada real al # método) en un único try/except que atrapaba InterpreterError # (= TesseractError), por lo que un conflicto de tipo real # (p.ej. push() violando una colección tipada) quedaba # silenciado en vez de propagarse. Ahora sólo se captura el # caso en que 'left' no es una variable declarada; cualquier # error lanzado por la ejecución real del método (incluido # TypeExplicitConflictError) se propaga normalmente. try: obj = self.symbol_table.get_value(left) except UndeclaredVariableError: obj = _UNRESOLVED if obj is not _UNRESOLVED: val_type = _get_value_type(obj) if val_type in _CORE_TYPE_METHODS: # Construir la cadena de llamada completa y delegar en resolve_expression # REPARADO: antes se usaba 'function_name' tal cual, pero cuando el # parser ya incluye los paréntesis vacíos en el nombre de la función # (ej. function_name == "arr.sort()"), esto producía una cadena con # paréntesis DUPLICADOS: "arr.sort()" + "()" = "arr.sort()()". Al # intentar evaluar eso, "sort()()" se interpretaba como llamar al # RESULTADO de sort() (una lista) como si fuera invocable, de ahí el # 'list' object is not callable. 'clean' (ya calculado arriba) tiene # los paréntesis finales despojados, así que reconstruimos desde ahí # para garantizar exactamente un solo par de paréntesis. args_node = node.get("arguments", {}) raw_args = args_node.get("value", "") if isinstance(args_node, dict) else "" # 'raw_args' normalmente es texto crudo, pero # _parse_call_parameters contempla explícitamente el caso # en que ya viene como una LISTA de Python pre-parseada # por el AST. Si eso pasa e interpolamos la lista # directo en el f-string, se obtiene su repr tal cual # (ej. ['"a"']) incrustado en call_str, corrompiendo el # texto. Si no es ya un string, la resolvemos primero # (soporta ambos formatos) y la volvemos a serializar # como literal Tesseract seguro. if isinstance(raw_args, str): args_text = raw_args else: _parsed_args = self._parse_call_parameters({"value": raw_args}) def _lit(_v): if isinstance(_v, str): return '"' + _v.replace('\\', '\\\\').replace('"', '\\"') + '"' if isinstance(_v, bool): return 'true' if _v else 'false' if _v is None: return 'null' return str(_v) args_text = ", ".join(_lit(_a) for _a in _parsed_args) # REPARADO (bug real de la corrupción "a"("a"): # 'function_name' NO siempre trae paréntesis vacíos al # final (ej. "arr.sort()"). Cuando la llamada original # ya tenía argumentos (ej. "arr.push(\"a\")"), # function_name viene con esos argumentos YA # incrustados. rstrip('()') sólo despoja paréntesis # sueltos al final y se DETIENE en cuanto encuentra un # carácter que no es paréntesis — en '"a")' eso es la # comilla de cierre, así que sólo se quitaba la ')' # final y quedaba 'arr.push("a"' pegado en 'clean'. Al # agregarle luego '(args_text)' encima, el argumento # quedaba DUPLICADO: 'arr.push("a"("a")'. # Solución: usar _split_method_chain (que sí respeta # paréntesis anidados) sobre 'right' para aislar el # NOMBRE del último método de la cadena, descartando # cualquier argumento viejo que ya trajera incrustado, # y reconstruir ese último paso usando solo la fuente # confiable de argumentos (args_text). Los pasos # previos de la cadena (si hay, ej. '.sort()' antes de # '.mut') se dejan intactos, tal cual venían. _steps = self._split_method_chain(right) if _steps: _last_step = _steps[-1] _last_name = _last_step.split('(', 1)[0].strip() # Defensivo: sólo agregar '(args_text)' si el paso # original YA era una llamada (tenía paréntesis). # Si era un atributo sin paréntesis (ej. '.mut' # escrito sin parens), se deja tal cual — si le # agregáramos '()' igual, dejaría de matchear el # chequeo especial 'step == "mut"' de # _execute_type_method_chain y rompería la mutación. if '(' in _last_step: _rebuilt = ".".join(_steps[:-1] + [f"{_last_name}({args_text})"]) else: _rebuilt = ".".join(_steps[:-1] + [_last_name]) else: _rebuilt = f"{right}({args_text})" call_str = f"{left}.{_rebuilt}" self._log(f"[CoreMethod] Delegando a resolve_expression: {call_str}") result = self.resolve_expression(call_str) return result # ── await func() ───────────────────────────────────────────────────── if isinstance(function_name, str) and function_name.startswith('await '): actual_fn = function_name[6:].strip() self._log(f"[Async] await detectado → '{actual_fn}'") return self._execute_async_call(actual_fn, node) # ── instance.method() o module.func() ──────────────────────────────── if isinstance(function_name, str) and '.' in function_name: clean = function_name.strip().rstrip('()') parts = clean.split('.', 1) if len(parts) == 2: left, right = parts # Primero: ¿es una instancia de clase? try: obj = self.symbol_table.get_value(left) if isinstance(obj, ClassInstance): args_node = node.get("arguments", {}) raw_args = args_node.get("value", "") if isinstance(args_node, dict) else "" args = self._parse_call_parameters({"value": raw_args}) if str(raw_args).strip() else [] self._log(f"[OOP] Llamada instancia: {left}.{right}({args})") method_info, origin = self.object_table.lookup_method(obj.class_name, right) ctx = MethodContext(self, method_info, obj, origin, args) return ctx.execute() if isinstance(obj, ModuleInstance): args_node = node.get("arguments", {}) raw_args = args_node.get("value", "") if isinstance(args_node, dict) else "" args = self._parse_call_parameters({"value": raw_args}) if str(raw_args).strip() else [] args = [a.native_obj if isinstance(a, ModuleInstance) else a for a in args] self._log(f"[Native] Llamada nativa: {left}.{right}({args})") try: return obj.call_method(right, args, self) except Exception as e: #raise InterpreterError(str(e), **self._lc()) self._log(f"\n❌ ERROR en método nativo '{right}': {e}") return None except TesseractError: raise except (UndeclaredVariableError, InterpreterError): pass # Segundo: ¿es un módulo (función libre)? if self.module_loader.is_loaded(left): arguments_node = node.get("arguments", {}) raw_args = arguments_node.get("value", "") if isinstance(arguments_node, dict) else "" args = self._parse_call_parameters({"value": raw_args}) if raw_args.strip() else [] try: fn = self.module_loader.get_function(left, right) return fn.call(args, self) except ModuleError as e: raise InterpreterError(str(e), **self._lc()) if function_name == "Break": self.break_flag = True return if function_name == "Return": return_value_node = node.get("value") or node.get("arguments") or {} return_value = None if "value" in return_value_node: raw = return_value_node["value"] # resolve_expression maneja todos los casos y lanza sus propias excepciones return_value = self.resolve_expression(raw) # ── Validar tipo de retorno si la función está tipada ───────── if self._current_function: try: fn_def = self.symbol_table.get_function(self._current_function) ret_type = fn_def.get("explicitType") if isinstance(fn_def, dict) else None if ret_type and ret_type not in ('inferred', 'dynamic', 'any', '', None): actual = _get_value_type(return_value) # Permitir promoción int → float if not (ret_type == 'float' and actual == 'int'): if actual != ret_type: raise ReturnTypeError( msg("func.return_mismatch", name=self._current_function, expected=ret_type, got=actual), **self._lc_here(str(raw) if raw is not None else "") ) except (UndeclaredVariableError, KeyError): pass # función no encontrada — no validar self._log(f"[TRACE] Return valor={return_value}") self.return_flag = True self.return_value = return_value return return_value # ── NATIVE CORE: print / read ───────────────────────────────────────── # arguments.value contiene TODOS los args como un solo string. # Ej: '"Hello, World!", end=""' -> hay que partirlo antes de resolver. if function_name == '__tesseract_config': return self._builtin_tesseract_config() if function_name == '__tesseract_capabilities': return self._builtin_tesseract_capabilities() if function_name in ('print', 'read'): _args_node = node.get('arguments') or {} _raw_all = _args_node.get('value', '') if isinstance(_args_node, dict) else (_args_node or '') _raw_all = str(_raw_all).strip() _parts = self._split_args_respecting_brackets(_raw_all) if _raw_all else [] _parts = [p.strip() for p in _parts if p.strip()] _ph = (node.get('paramType') or {}) _ph = _ph.get('value') if isinstance(_ph, dict) else _ph if function_name == 'print': return self._handle_print_args(_parts, _ph) else: return self._handle_read_args(_parts) arguments_node = node.get("arguments") param_type_node = node.get("paramType") raw_argument = arguments_node.get("value") if isinstance(arguments_node, dict) else arguments_node actual_param_type = param_type_node.get("value") if isinstance(param_type_node, dict) else param_type_node value_to_process = None if actual_param_type == "ModuleVariable": value_to_process = self.resolve_expression(raw_argument) elif actual_param_type == "function" and isinstance(raw_argument, str) and '.' in raw_argument: value_to_process = self.resolve_expression(raw_argument) elif function_name == "print" and isinstance(raw_argument, str) and self._is_function_call(raw_argument): self._log(f"[TRACE] print con llamada a función: '{raw_argument}'") value_to_process = self.resolve_expression(raw_argument) elif actual_param_type == "ArrayAccess": if isinstance(raw_argument, str) and re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', raw_argument): try: variable_value = self.symbol_table.get_value(raw_argument) if isinstance(variable_value, str) and '[' in variable_value: value_to_process = self._evaluate_array_access(variable_value) else: value_to_process = variable_value except UndeclaredVariableError: value_to_process = f"" else: value_to_process = self._evaluate_array_access(raw_argument) elif actual_param_type == "expression": value_to_process = self.evaluate_expression(raw_argument) elif isinstance(raw_argument, str): if actual_param_type == 'string': if raw_argument.startswith('"') and raw_argument.endswith('"'): value_to_process = self._interpolate_hash_string(raw_argument[1:-1]) elif any(op in raw_argument for op in ['+', '-', '*', '/', '%', '(', ')']): value_to_process = self.resolve_expression(raw_argument) elif '.' in raw_argument and not raw_argument.replace('.', '', 1).isdigit(): value_to_process = self.resolve_expression(raw_argument) else: if hasattr(self, '_current_function') and self._current_function: function_params = self.symbol_table.get_function_params(self._current_function) if raw_argument in function_params: try: value_to_process = self.symbol_table.get_function_param_value( self._current_function, raw_argument) except UndeclaredVariableError: pass if value_to_process is None: try: value_to_process = self.symbol_table.get_value(raw_argument) except UndeclaredVariableError: value_to_process = raw_argument elif any(op in raw_argument for op in ['+', '-', '*', '/', '%']): value_to_process = self.resolve_expression(raw_argument) elif '.' in raw_argument: value_to_process = self.resolve_expression(raw_argument) else: if hasattr(self, '_current_function') and self._current_function: function_params = self.symbol_table.get_function_params(self._current_function) if raw_argument in function_params: try: value_to_process = self.symbol_table.get_function_param_value( self._current_function, raw_argument) except UndeclaredVariableError: pass if value_to_process is None: try: value_to_process = self.symbol_table.get_value(raw_argument) except UndeclaredVariableError: value_to_process = raw_argument else: value_to_process = raw_argument # (print y read interceptados arriba - no llegan aqui) # ========================================================================== # Funciones nativas core: print y read # ========================================================================== def _handle_print_args(self, parts, type_hint=None): """ Maneja print() con los argumentos ya partidos por coma de nivel 0. print() print(, end="...") print() <- solo salto de linea Segundo arg puede ser posicional o named (end=...). """ if not parts: # print() vacío — si el anterior no dejó \n, agregarlo if getattr(self, '_last_print_end', '\n') != '\n': print() self._last_print_end = '\n' else: print() return value = self._resolve_print_value(parts[0], type_hint) end = '\n' for part in parts[1:]: p = part.strip() m = re.match(r'(?i)^end\s*=\s*(.+)$', p) end_val = m.group(1).strip() if m else p if (end_val.startswith('"') and end_val.endswith('"')) or \ (end_val.startswith("'") and end_val.endswith("'")): end_val = end_val[1:-1] end = (end_val.replace('\\n', '\n') .replace('\\t', '\t') .replace('\\r', '\r') .replace('\\\\', '\\')) break # solo el primero extra es end self._do_print(value, end) def _resolve_print_value(self, raw, type_hint=None): """ Resuelve el primer argumento de print() a su valor Python. Orden: string literal -> bool/null -> numero -> variable -> expresion. """ if raw is None: return None raw = str(raw).strip() if not raw: return None # Literal string — solo si NO contiene separador de concatenación # '"a" . "b"' empieza y termina con " pero es concatenación is_simple_string = ( ((raw.startswith('"') and raw.endswith('"')) or (raw.startswith("'") and raw.endswith("'"))) and ' . ' not in raw # no es concatenación con espacios ) if is_simple_string: inner = raw[1:-1] inner = (inner.replace('\\n', '\n') .replace('\\t', '\t') .replace('\\r', '\r') .replace('\\\""', '"') .replace("\\'", "'") .replace('\\\\', '\\')) return self._interpolate_hash_string(inner) if raw == 'true': return True if raw == 'false': return False if raw in ('null', 'NULL'): return None try: return float(raw) if '.' in raw else int(raw) except (ValueError, TypeError): pass # Identificador simple: params de funcion primero, luego symbol_table if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', raw): if hasattr(self, '_current_function') and self._current_function: fps = self.symbol_table.get_function_params(self._current_function) if raw in fps: try: return self.symbol_table.get_function_param_value( self._current_function, raw) except UndeclaredVariableError: pass try: val = self.symbol_table.get_value(raw) # ── AZÚCAR: print(p1) → persona ────────── if isinstance(val, StructInstance): return val.format_print(raw) return val except UndeclaredVariableError: raise NameError( msg("name.undefined", name=raw), **self._lc_here(raw) ) # Expresion compleja try: val = self.resolve_expression(raw) # ── AZÚCAR: print(p1.dir) → direccion ── if isinstance(val, StructInstance): return val.format_print(raw) return val except TesseractError: raise # propagar errores del sistema except Exception: return raw def _do_print(self, value, end='\n'): """ Formatea e imprime con el estilo del lenguaje. None -> null bool -> true / false float -> sin ceros finales superfluos list -> [a, b, c] str -> sin comillas envolventes, escapes procesados """ def _fmt(v): if v is None: return 'null' if isinstance(v, RangeValue): return v.format_display() if isinstance(v, bool): return 'true' if v else 'false' if isinstance(v, float): s = f'{v:.15f}'.rstrip('0') return s if not s.endswith('.') else s + '0' if isinstance(v, tuple): return '(' + ', '.join(_fmt(i) for i in v) + ')' if isinstance(v, list): return '[' + ', '.join(_fmt(i) for i in v) + ']' if isinstance(v, dict): return '{' + ', '.join(f"{k}:{_fmt(vv)}" for k, vv in v.items()) + '}' if isinstance(v, str): # AGREGADO: los elementos string DENTRO de una colección # (array/tupla/dict) se muestran entrecomillados, para no # confundirlos con otros tipos. Aplica también en # colecciones anidadas, porque esta misma _fmt es la que # se llama recursivamente arriba. # REPARADO: antes usaba json.dumps, que escapa TODO # caracter no-ASCII a \uXXXX (ensure_ascii=True) — eso # sólo debe pasar cuando se pide explícitamente con .uni, # no en un print normal. _quote_for_display muestra el # carácter real y sólo sustituye por ▯ los que de verdad # son no-imprimibles (controles, etc.). return _quote_for_display(v) return str(v) if value is None: out = 'null' elif isinstance(value, RangeValue): out = value.format_display() elif isinstance(value, bool): out = 'true' if value else 'false' elif isinstance(value, float): s = f'{value:.15f}'.rstrip('0') out = s if not s.endswith('.') else s + '0' elif isinstance(value, StructInstance): # Safety net: format_print should have been called already in _resolve_print_value out = repr(value) elif isinstance(value, tuple): out = '(' + ', '.join(_fmt(i) for i in value) + ')' elif isinstance(value, list): out = '[' + ', '.join(_fmt(i) for i in value) + ']' elif isinstance(value, dict): out = '{' + ', '.join(f"{k}:{_fmt(v)}" for k, v in value.items()) + '}' elif isinstance(value, str): s = value if len(s) >= 2 and ((s[0] == '"' and s[-1] == '"') or (s[0] == "'" and s[-1] == "'")): s = s[1:-1] out = s else: out = str(value) # Si el print anterior no terminó en \n y este sí, agregar \n primero _prev_end = getattr(self, '_last_print_end', '\n') if _prev_end != '\n' and end == '\n': print() # restaurar cursor a nueva línea antes de imprimir print(out, end=end) self._last_print_end = end def _handle_read_args(self, parts): """ Maneja read() con los argumentos ya partidos por coma de nivel 0. read() read(, ) read(, , ) read(, ) <- msg detectado como string literal read(void) <- AGREGADO: getchar() de C (lee 1 char y lo descarta) read(void, ) <- getchar() con prompt (msg se muestra sin salto de línea) read(, void) <- tipo void = getchar(), igual que arriba read(, type=int, msg="texto:") Tipos: int float bool string (default) void Bool: \\true \\false true false 1 0 """ VALID_TYPES = {'int', 'float', 'bool', 'string', 'void'} if not parts: raise InterpreterError("read() requiere al menos un argumento (variable o void).", **self._lc()) entry_val = None input_type = 'string' message = '' is_void = False for i, part in enumerate(parts): p = part.strip() # Named: type=... m = re.match(r'(?i)^type\s*=\s*(.+)$', p) if m: t = m.group(1).strip().lower() if t not in VALID_TYPES: raise InterpreterError( f"read: tipo invalido '{t}'. Valores validos: int, float, bool, string, void.", **self._lc()) if t == 'void': is_void = True else: input_type = t continue # Named: msg=... m = re.match(r'(?i)^msg\s*=\s*(.+)$', p) if m: v = m.group(1).strip() if (v.startswith('"') and v.endswith('"')) or \ (v.startswith("'") and v.endswith("'")): v = v[1:-1] v = (v.replace('\\n', '\n').replace('\\t', '\t') .replace('\\\""', '"').replace("\\'", "'") .replace('\\\\', '\\')) message = self._interpolate_hash_string(v) continue # Posicional 0: variable o void if i == 0: if p == 'void': is_void = True else: entry_val = p continue # Posicional resto: tipo conocido if p.lower() in VALID_TYPES: t = p.lower() if t == 'void': is_void = True else: input_type = t continue # Posicional resto: string literal -> mensaje if (p.startswith('"') and p.endswith('"')) or \ (p.startswith("'") and p.endswith("'")): v = p[1:-1] v = (v.replace('\\n', '\n').replace('\\t', '\t') .replace('\\\""', '"').replace("\\'", "'") .replace('\\\\', '\\')) message = self._interpolate_hash_string(v) continue # Posicional resto: variable que contiene el mensaje try: resolved = self.symbol_table.get_value(p) if isinstance(resolved, str): message = resolved except Exception: pass # ── void: comportamiento tipo getchar() de C ───────────────────── # AGREGADO: en vez de leer una línea completa con input() y # descartarla, void ahora lee UN SOLO CARÁCTER de la entrada # estándar (igual que getchar() en C), y lo descarta (no hay # variable a la que asignarlo). Sigue siendo compatible con msg: # si se dio un mensaje, se muestra como prompt (sin salto de # línea) antes de leer el carácter. if is_void: if message: print(message, end='', flush=True) try: ch = sys.stdin.read(1) except Exception: ch = '' self._log(f"[read/void] getchar() leyó {ch!r} (descartado)") return try: user_input = input(message) except EOFError: user_input = '' final_value = self._convert_read_input(user_input, input_type) if entry_val: try: self.symbol_table.set_value(entry_val, final_value) except UndeclaredVariableError: self.symbol_table.declare(entry_val, Symbol(final_value, 'dynamic')) self._log(f"[read] '{entry_val}' = {final_value!r} (tipo: {input_type})") # ══════════════════════════════════════════════════════════════════════ # AGREGADO: read() usado como EXPRESIÓN (var x = read(...); / x = read(...);) # ══════════════════════════════════════════════════════════════════════ def _handle_read_args_as_expression(self, parts): """ Variante de _handle_read_args() para cuando read() se usa como EXPRESIÓN — su valor lo asigna la declaración/asignación que lo contiene, en vez de pasar la variable como primer argumento: var x = read(); var x = read(int); x = read(msg="entrada: "); x = read(int, "entrada: "); x = read(type=int, msg="entrada: "); La forma de SENTENCIA (read(x, int); con la variable como primer argumento) sigue funcionando exactamente igual — ver _handle_read_args(), sin tocar. Aquí, en cambio, NO hay un primer argumento reservado para el nombre de variable: TODOS los argumentos son opcionales y sólo pueden ser el tipo (primitivo) y/o el mensaje, con las mismas reglas de detección (named type=/msg=, posicional tipo conocido, posicional string literal = mensaje, posicional variable-string = mensaje) que ya usa la forma de sentencia. 'void' no aplica aquí — no tiene sentido descartar el valor de una expresión que se va a asignar. Tipos: int float bool string (default) Devuelve el valor ya convertido, para que lo use quien resolvió esta expresión (VariableDeclaration/VariableAsignement/etc.). """ VALID_TYPES = {'int', 'float', 'bool', 'string'} input_type = 'string' message = '' for part in parts: p = part.strip() # Named: type=... m = re.match(r'(?i)^type\s*=\s*(.+)$', p) if m: t = m.group(1).strip().lower() if t not in VALID_TYPES: raise InterpreterError( f"read: tipo invalido '{t}'. Valores validos: int, float, bool, string.", **self._lc()) input_type = t continue # Named: msg=... m = re.match(r'(?i)^msg\s*=\s*(.+)$', p) if m: v = m.group(1).strip() if (v.startswith('"') and v.endswith('"')) or \ (v.startswith("'") and v.endswith("'")): v = v[1:-1] v = (v.replace('\\n', '\n').replace('\\t', '\t') .replace('\\\""', '"').replace("\\'", "'") .replace('\\\\', '\\')) message = self._interpolate_hash_string(v) continue # Posicional: tipo conocido if p.lower() in VALID_TYPES: input_type = p.lower() continue # Posicional: string literal -> mensaje if (p.startswith('"') and p.endswith('"')) or \ (p.startswith("'") and p.endswith("'")): v = p[1:-1] v = (v.replace('\\n', '\n').replace('\\t', '\t') .replace('\\\""', '"').replace("\\'", "'") .replace('\\\\', '\\')) message = self._interpolate_hash_string(v) continue # Posicional: variable que contiene el mensaje try: resolved = self.symbol_table.get_value(p) if isinstance(resolved, str): message = resolved except Exception: pass try: user_input = input(message) except EOFError: user_input = '' return self._convert_read_input(user_input, input_type) def _convert_read_input(self, raw_str, input_type): """Convierte el string leido de consola al tipo indicado.""" s = raw_str.strip() if input_type == 'int': try: return int(float(s)) except (ValueError, TypeError): raise InterpreterError(f"read: no se pudo convertir '{s}' a int.", **self._lc()) if input_type == 'float': try: return float(s) except (ValueError, TypeError): raise InterpreterError(f"read: no se pudo convertir '{s}' a float.", **self._lc()) if input_type == 'bool': norm = s.lower().lstrip('\\') if norm in ('true', '1'): return True if norm in ('false', '0'): return False raise InterpreterError("read: valor bool invalido. Usa \\true, \\false, 1 o 0.", **self._lc()) return raw_str def _infer_type(self, value): """Infiere el tipo de un valor""" if isinstance(value, bool): return "bool" elif isinstance(value, int): return "int" elif isinstance(value, float): return "float" elif isinstance(value, str): return "string" else: return "any" def handle_Function(self, node): name = node["name"] parameters_info = self._parse_function_parameters(node.get("parameters", {})) is_async = bool(node.get('async') or node.get('isAsync')) # ── Punto de entrada: function myfunc() -> main {} ──────────────── target = node.get("target", "") if target == "main": if self._main_function is not None: raise InterpreterError( f"Solo puede haber una función de entrada (->main). " f"Ya existe '{self._main_function}', se encontró otra en '{name}'.", **self._lc()) self._main_function = name self._log(f"[MAIN] Función de entrada registrada: '{name}'") self.symbol_table.declare_function(name, node) self.symbol_table.declare_function_params(name, parameters_info) if is_async: self._async_functions.add(name) self._log(f"[Async] Función async '{name}' registrada.") return_type = node.get("explicitType", "inferred") self._log(f"[TRACE] Función '{name}' declarada | params={parameters_info} | retorno={return_type} | async={is_async} | target={target or 'none'}") def _parse_function_parameters(self, parameters_node): """ Parsea parámetros de función. Soporta dos formatos: Nuevo (actual): "string a, int b" → type antes del nombre Viejo (legado): "a:string, b:int" → nombre:tipo Sin tipo: "a, b" → dinámico Devuelve dict {param_name: param_type} donde tipo es 'any' si no hay tipo. """ # Tipos primitivos reconocidos por Tesseract _KNOWN_TYPES = frozenset({ 'string', 'int', 'float', 'bool', 'array', 'tuple', 'dict', 'null', 'any', 'dynamic', }) params_info = {} if "value" not in parameters_node: return params_info params_str = parameters_node["value"] if not params_str: return params_info param_parts = [p.strip() for p in params_str.split(',') if p.strip()] for param_part in param_parts: # ── Formato legado: nombre:tipo ───────────────────────────────────── if ':' in param_part: param_name, param_type = param_part.split(':', 1) params_info[param_name.strip()] = param_type.strip() continue tokens = param_part.split() # ── Formato nuevo: tipo nombre (ej. "string a") ───────────────────── if len(tokens) == 2 and tokens[0].lower() in _KNOWN_TYPES: param_type = tokens[0].lower() param_name = tokens[1] params_info[param_name] = param_type continue # ── Sin tipo: parámetro dinámico ──────────────────────────────────── params_info[param_part.strip()] = "any" return params_info def handle_FunctionCall(self, node): function_name = node["function"] self._log(f"Intentando llamar a la función: '{function_name}'") # Obtener argumentos de la llamada call_parameters = self._parse_call_parameters(node.get("paramenters", {})) # Crear contexto de función y ejecutar function_context = FunctionContext(self, function_name, call_parameters, call_node=node) result = function_context.execute_function() # ===== PROCESAR VALOR DE RETORNO ===== if result is not None: if isinstance(result, str): # Verificar si es una llamada a función anidada if self._is_function_call(result): result = self._execute_function_call_from_string(result) # Verificar si NO es un literal string elif not (result.startswith('"') and result.endswith('"')): # Intentar como expresión si tiene operadores if any(op in result for op in ['+', '-', '*', '/', '%', '.']): result = self.resolve_expression(result) else: # Intentar obtener como variable try: result = self.symbol_table.get_value(result) except UndeclaredVariableError: # Mantener valor original pass self._log(f"Función '{function_name}' retornó: {result}") return result self._log(f"Función '{function_name}' ejecutada sin retorno explícito") return None def _split_args_respecting_brackets(self, args_str): """Divide por comas respetando [], (), {}, y strings entre comillas.""" parts, current, depth, in_str, str_char = [], [], 0, False, '"' i = 0 while i < len(args_str): ch = args_str[i] if in_str: current.append(ch) if ch == str_char and (i == 0 or args_str[i-1] != '\\'): in_str = False elif ch in ('"', "'"): in_str = True str_char = ch current.append(ch) elif ch in ('(', '[', '{'): depth += 1; current.append(ch) elif ch in (')', ']', '}'): depth -= 1; current.append(ch) elif ch == ',' and depth == 0: parts.append(''.join(current)); current = [] else: current.append(ch) i += 1 if current: parts.append(''.join(current)) return [p.strip() for p in parts if p.strip()] def _parse_call_parameters(self, parameters_node): """ Versión canónica unificada. Maneja: - Lista Python del AST → evalúa cada elemento directamente - Escalar no-string → devuelve [valor] - String vacío → [] - String con args → divide por coma, respeta [], (), {}, strings El formato "nombre:valor" (tipado Tesseract) extrae solo el valor. """ params = [] if "value" not in parameters_node: return params params_str = parameters_node["value"] # Vacío if params_str is None or params_str == "" or params_str == []: return params # Lista Python presuelta por el AST if isinstance(params_str, list): for item in params_str: if isinstance(item, (int, float, bool)) or item is None: params.append(item) elif isinstance(item, str): params.append(self._evaluate_parameter_value(item)) else: params.append(item) return params # Escalar no-string (int, float, bool directo del AST) if not isinstance(params_str, str): return [params_str] if not params_str.strip(): return params param_parts = self._split_args_respecting_brackets(params_str) for param_part in param_parts: param_part = param_part.strip() if not param_part: continue is_literal_string = (param_part.startswith('"') and param_part.endswith('"')) or \ (param_part.startswith("'") and param_part.endswith("'")) if ':' in param_part and not is_literal_string: _, value_part = param_part.split(':', 1) param_value = self._evaluate_parameter_value(value_part.strip()) else: param_value = self._evaluate_parameter_value(param_part) params.append(param_value) return params def handle_LibraryCall(self, node): module_raw = node["module"] alias = node.get("alias") functions = node.get("functions") func_aliases = node.get("functionAliases", []) inline_aliases = node.get("inlineAliases", {}) on_names = node.get("on", []) on_aliases = node.get("onAliases", []) # ── ¿Es un archivo fuente .tss? ──────────────────────────────────── if self._is_source_file_import(module_raw): self._load_source_file(module_raw, alias, functions, func_aliases, inline_aliases) return # ── Módulo nativo (comportamiento original) ──────────────────────── module_name = module_raw self._log(f"Cargando módulo nativo '{module_name}'") try: if functions: # from math use PI, sqrt → cargar solo lo pedido self.module_loader.load_selective(module_name, functions) else: # library math → cargar todo self.module_loader.load(module_name) self._register_native_classes(module_name) except ModuleError as e: raise ImportError( msg("import.not_found", module=module_name), **self._lc(node) ) if alias: self.module_loader.apply_alias(module_name, alias) if functions: if func_aliases: self.module_loader.apply_member_aliases_positional( module_name, functions, func_aliases ) else: for fname, falias in inline_aliases.items(): self.module_loader.apply_member_alias_inline( module_name, fname, falias ) # ── Inyección selectiva: from math use PI, sqrt ─────────────── # Solo inyectar los miembros pedidos en la tabla de símbolos self._inject_native_module_symbols(module_name, functions, inline_aliases, func_aliases) if on_names: self.module_loader.apply_on_aliases(module_name, on_names, on_aliases) self._log(f"Módulo nativo '{module_name}' listo.") def _inject_native_module_symbols(self, module_name: str, members: list, inline_aliases: dict, func_aliases: list): """ Inyecta en la tabla de símbolos principal solo los miembros pedidos de un módulo nativo. from math use PI → PI disponible directamente from math use sqrt al sqr → sqr disponible directamente """ pos_alias = dict(zip(members, func_aliases)) if func_aliases else {} mod = self.module_loader.get_module(module_name) for orig_name in members: exposed = (inline_aliases.get(orig_name) or pos_alias.get(orig_name) or orig_name) # ¿Es una función del módulo? try: mf = mod.get_function(orig_name) # Registrar como función en la tabla # Creamos un nodo sintético compatible con declare_function fake_node = { 'name': exposed, 'parameters': mf.params, 'block': mf.block if mf.kind == 'script' else [], '_native_module': module_name, '_native_fn_name': orig_name, } # Guardamos referencia directa al ModuleFunction para llamarla self.symbol_table.declare( exposed, Symbol(value=mf, declared_type='function')) self._log(f" Importada función nativa '{orig_name}' como '{exposed}'") continue except Exception: pass # ¿Es un export (variable/constante)? try: exp = mod.get_export(orig_name) sym = Symbol(value=exp.value, declared_type=exp.export_type or 'dynamic', is_const=exp.is_const) self.symbol_table.declare(exposed, sym) self._log(f" Importado export '{orig_name}' = {exp.value!r} como '{exposed}'") continue except Exception: pass self._log(f" ADVERTENCIA: '{orig_name}' no encontrado en módulo '{module_name}'") # ========================================================================== # OOP — Declaración de Clase # ========================================================================== def handle_ClassDeclaration(self, node): class_name = node.get('name') self._log(f"[OOP] === Declarando clase '{class_name}' ===") parent = node.get('extends') impl_raw = node.get('implements') modifier = node.get('modifier') interfaces = [i.strip() for i in impl_raw.split(',')] if isinstance(impl_raw, str) else [] class_def = ClassDefinition( name=class_name, parent=parent, interfaces=interfaces, modifier=modifier ) members = node.get('members', node.get('block', [])) self._log(f"[OOP] Procesando {len(members)} miembros de '{class_name}'") for member in members: self._process_class_member(member, class_def) self.object_table.declare_class(class_name, class_def) self._log(f"[OOP] Clase registrada: {class_def}") def _process_class_member(self, member: dict, class_def: ClassDefinition): """Parsea un miembro (atributo, método o constructor) y lo añade a class_def.""" # Desenvolver ClassMember wrapper si existe if 'ClassMember' in member: inner = member['ClassMember'] for key, content in inner.items(): self._process_class_member({key: content}, class_def) return # ── Atributo ──────────────────────────────────────────────────────── attr_kinds = {'AttributeDeclaration', 'AttributeConstantDeclaration', 'ConstantAttributeDeclaration'} matched = attr_kinds & set(member.keys()) if matched: kind = matched.pop() attr_node = member[kind] attr_name = attr_node.get('name') val_node = attr_node.get('value', {}) raw_val = val_node.get('value') if isinstance(val_node, dict) else None modifier = attr_node.get('modifier', 'public') is_const = 'Constant' in kind or 'constant' in kind.lower() attr_type = val_node.get('type') if isinstance(val_node, dict) else None default_value = None if raw_val is not None: try: default_value = (self.resolve_expression(str(raw_val)) if isinstance(raw_val, str) else raw_val) except Exception: default_value = raw_val class_def.attributes[attr_name] = { 'value': default_value, 'type': attr_type, 'modifier': modifier, 'is_const': is_const, } self._log(f"[OOP] Atributo '{attr_name}' ({modifier}) default={default_value}") return # ── Método ────────────────────────────────────────────────────────── if 'MethodDeclaration' in member: mnode = member['MethodDeclaration'] method_name = mnode.get('name') is_async = bool(mnode.get('async') or mnode.get('isAsync')) params_info = self._parse_function_parameters(mnode.get('parameters', {})) mod = mnode.get('modifier', 'public') etype = mnode.get('explicitType') class_def.methods[method_name] = { 'node': mnode, 'params': params_info, 'modifier': mod, 'explicit_type': etype, 'is_async': is_async, } self._log(f"[OOP] Método '{method_name}' ({mod}) params={list(params_info.keys())} async={is_async}") return # ── Constructor ───────────────────────────────────────────────────── if 'ConstructorDeclaration' in member: cnode = member['ConstructorDeclaration'] params_info = self._parse_function_parameters(cnode.get('parameters', {})) mod = cnode.get('modifier', 'public') class_def.constructor = { 'node': cnode, 'params': params_info, 'modifier': mod, } self._log(f"[OOP] Constructor ({mod}) params={list(params_info.keys())}") return # ========================================================================== # OOP — Declaración de Interfaz # ========================================================================== def handle_InterfaceDeclaration(self, node): name = node.get('name') self._log(f"[OOP] Declarando interfaz '{name}'") self.object_table.declare_interface(name, node) # ========================================================================== # OOP — new ClassName(args) # ========================================================================== def handle_NewObject(self, node): class_name = node.get('class') args_node = node.get('arguments', {}) raw_args = args_node.get('value', '') if isinstance(args_node, dict) else '' args = self._parse_call_parameters({'value': raw_args}) if str(raw_args).strip() else [] return self._instantiate_object(class_name, args) def _instantiate_object(self, class_name: str, args: list): """Instancia una clase (TSS o nativa).""" # 1. Clase definida en TSS if self.object_table.has_class(class_name): instance = self.object_table.instantiate(class_name) ctor = self._find_constructor(class_name) if ctor: self._execute_constructor(ctor, instance, args) elif args: self._log(f"[OOP] ADVERTENCIA: '{class_name}' sin constructor, args ignorados.") self._log(f"[OOP] Instancia TSS: {instance}") return instance # 2. Clase nativa (cargada por ModuleLoader) if self.module_loader.has_class(class_name): return self._instantiate_native_class(class_name, args) raise InterpreterError(f"Clase '{class_name}' no declarada.", **self._lc()) def _instantiate_native_class(self, class_name: str, args: list): mod_name, module_class = self.module_loader.get_class_info(class_name) self._log(f"[OOP] Instanciando clase nativa '{class_name}' desde módulo '{mod_name}'") instance = self.module_loader.instantiate(mod_name, class_name, args, self) # Parche para labels: forzar un tamaño mínimo if class_name == "Label": try: instance.call_method("setSize", [100, 25], self) except Exception: pass return instance def _find_constructor(self, class_name: str): """Busca constructor subiendo por la jerarquía.""" visited, current = set(), class_name while current and current not in visited: visited.add(current) if current in self.object_table.class_definitions: cls = self.object_table.class_definitions[current] if cls.constructor: return cls.constructor current = cls.parent else: break return None def _execute_constructor(self, ctor_info: dict, instance: ClassInstance, args: list): ctor_node = ctor_info['node'] func_params = ctor_info['params'] class_name = instance.class_name frame_key = f"{class_name}.__construct__" old_fn = self._current_function old_inst = self._current_instance self._current_function = frame_key self._current_instance = instance self.symbol_table.push_function_frame(frame_key) self.symbol_table.push_scope(label=f"Constructor '{class_name}'") param_names = list(func_params.keys()) for i, pname in enumerate(param_names): if i < len(args): self.symbol_table.declare_function_param_value(frame_key, pname, args[i]) self._log(f"[OOP] ctor param '{pname}' = {args[i]}") old_ret = self.return_flag self.return_flag = False block = ctor_node.get('block', []) if isinstance(block, list): for stmt in block: if self.return_flag: break self.execute_node(stmt) self.return_flag = old_ret self._current_function = old_fn self._current_instance = old_inst self.symbol_table.pop_scope(label=f"Constructor '{class_name}'") self.symbol_table.pop_function_frame(frame_key) self._log(f"[OOP] Constructor '{class_name}' finalizado.") # ========================================================================== # OOP — this / super # ========================================================================== def _lc(self, node: dict = None) -> dict: """ Extrae line/col/file del nodo AST para pasarlos a excepciones. Si no se pasa nodo, usa self._current_line/_current_col rastreados por execute_node. """ if isinstance(node, dict): lc = node.get("linecolumn", {}) line = lc.get("line") or self._current_line col = lc.get("col") or self._current_col else: line = self._current_line col = self._current_col return { "file": self._source_file, "line": line, "col": col, "function": self._current_function, } def _lc_at(self, node, target, occurrence=1): """ Como _lc(node), pero recalcula 'col' para que el cursor '^^^' del traceback apunte exactamente al fragmento de texto 'target' dentro del 'source' de esa línea, en vez de usar el 'col' crudo del AST (que es inconsistente: a veces cae en el '=', a veces al final de un identificador, a veces en el ';'). target: substring exacto a localizar en linecolumn['source'] (ej. el nombre de una variable, un operador, un literal). occurrence: si target aparece más de una vez en la línea, qué ocurrencia usar (1 = la primera). Si no se puede ubicar 'target' en 'source' (no hay source, no aparece, etc.), cae de vuelta al col original del nodo — nunca falla, nunca lanza, siempre devuelve algo usable. """ base = self._lc(node) lc = node.get("linecolumn", {}) if isinstance(node, dict) else {} source = lc.get("source") if isinstance(source, str) and isinstance(target, str) and target: idx = -1 search_from = 0 for _ in range(max(occurrence, 1)): found = source.find(target, search_from) if found == -1: idx = -1 break idx = found search_from = found + 1 if idx != -1: base["col"] = idx + 1 # 1-based, igual convención que el resto del AST return base def _lc_here(self, target, occurrence=1): """ Como _lc_at, pero para errores que ocurren DENTRO de una expresión evaluada como texto plano (resolve_expression / evaluate_expression), donde no hay un nodo AST propio para el token que falló — solo el texto del token (ej. el operando inválido de una suma, el nombre de una variable no definida dentro de "a + b"). Usa self._current_line (la línea de la instrucción que contiene la expresión, ya correcta) y busca 'target' dentro de la línea real en self._source_lines para apuntar el cursor exactamente al fragmento que causó el error, en vez de a la instrucción completa. Si no se puede ubicar (sin source_lines, target no aparece, etc.) cae de vuelta a self._current_line/_current_col tal cual los daba _lc() antes de este cambio — nunca falla, nunca lanza. """ base = self._lc() line = base.get("line") if (isinstance(target, str) and target and isinstance(self._source_lines, list) and isinstance(line, int) and 1 <= line <= len(self._source_lines)): source = self._source_lines[line - 1] idx = -1 search_from = 0 for _ in range(max(occurrence, 1)): found = source.find(target, search_from) if found == -1: idx = -1 break idx = found search_from = found + 1 if idx != -1: base["col"] = idx + 1 return base def _require_instance(self, keyword='this'): if not self._current_instance: raise ScopeError( msg("scope.invalid_access", name=keyword), **self._lc() ) def handle_ThisAccess(self, node): self._require_instance('this') raw = node.get('value', '') field = raw.split('.', 1)[1] if '.' in raw else raw val = self._current_instance.get_attribute(field) self._log(f"[OOP] this.{field} = {val}") return val def handle_ThisCall(self, node): self._require_instance('this') raw = node.get('value', '') method_name = raw.split('.', 1)[1] if '.' in raw else raw args_node = node.get('arguments', {}) raw_args = args_node.get('value', '') if isinstance(args_node, dict) else '' args = self._parse_call_parameters({'value': raw_args}) if str(raw_args).strip() else [] self._log(f"[OOP] this.{method_name}({args})") method_info, origin = self.object_table.lookup_method( self._current_instance.class_name, method_name) return MethodContext(self, method_info, self._current_instance, origin, args).execute() def handle_ThisAssignment(self, node): self._require_instance('this') # El nodo puede venir del decoder (.tbc) con campo "member" # o del parser directamente con campo "value" raw = node.get('member', node.get('value', '')) field = raw.split('.', 1)[1] if '.' in raw else raw # El RHS puede estar en varios campos según el origen del nodo val_expr = node.get('value', '') # en el .tbc "value" es el RHS assigned = node.get('assigned', {}) raw_val = None # Detectar auto-referencia: parser bug donde el RHS se pierde # Ej: this._bridge = __linker_create(path) → solo queda "this._bridge" is_self_ref = val_expr in (raw, f"this.{field}", field, "") if not is_self_ref: # Hay RHS real if isinstance(assigned, dict) and assigned: raw_val = assigned.get('value') if raw_val is None: raw_val = node.get('value_expr') or node.get('expression') if raw_val is None and val_expr: raw_val = val_expr value = None if raw_val is not None and str(raw_val).strip() not in ('', 'null', 'NULL'): try: value = (self.resolve_expression(str(raw_val)) if isinstance(raw_val, str) else raw_val) except Exception: value = raw_val # Si es auto-referencia (parser bug), intentar inicializar bridge # buscando la función ___create o __linker_create en el scope if value is None and is_self_ref and self._current_instance is not None: class_name = self._current_instance.class_name candidates = [ "__linker_create", f"__{class_name.lower()}_create", f"__{class_name}_create", ] for fn_name in candidates: try: fn = None try: fn = self.symbol_table.get_function(fn_name) except Exception: pass if fn is None: try: fn_val = self.symbol_table.get_value(fn_name) if hasattr(fn_val, 'call'): fn = fn_val except Exception: pass if fn is None: continue # Obtener el primer argumento disponible en el scope # (normalmente "path" del constructor) ctor_args = [] for pname in ('path', 'p', 'filepath', 'src', 'url'): try: ctor_args.append(self.symbol_table.get_value(pname)) break except Exception: pass if ctor_args: value = fn.call(ctor_args, self) self._log( f"[OOP] Bridge '{field}' inicializado via " f"{fn_name}({ctor_args[0]!r})" ) break except Exception: pass self._current_instance.set_attribute(field, value) self._log(f"[OOP] this.{field} = {value!r}") def handle_SuperAccess(self, node): self._require_instance('super') raw = node.get('value', '') field = raw.split('.', 1)[1] if '.' in raw else raw parent = self.object_table.class_definitions[self._current_instance.class_name].parent if not parent: raise InterpreterError(f"'{self._current_instance.class_name}' no tiene clase padre.", **self._lc()) val = self._current_instance.attributes.get(field) self._log(f"[OOP] super.{field} = {val}") return val def handle_SuperCall(self, node): self._require_instance('super') raw = node.get('value', '') method_name = raw.split('.', 1)[1] if '.' in raw else raw args_node = node.get('arguments', {}) raw_args = args_node.get('value', '') if isinstance(args_node, dict) else '' args = self._parse_call_parameters({'value': raw_args}) if str(raw_args).strip() else [] parent = self.object_table.class_definitions[self._current_instance.class_name].parent if not parent: raise InterpreterError(f"'{self._current_instance.class_name}' no tiene clase padre.", **self._lc()) self._log(f"[OOP] super.{method_name}({args})") method_info, origin = self.object_table.lookup_method(parent, method_name) return MethodContext(self, method_info, self._current_instance, origin, args).execute() def handle_SuperAssignment(self, node): self._require_instance('super') raw = node.get('value', '') field = raw.split('.', 1)[1] if '.' in raw else raw assigned = node.get('assigned', {}) raw_val = assigned.get('value') if isinstance(assigned, dict) else None value = None if raw_val is not None: try: value = self.resolve_expression(str(raw_val)) if isinstance(raw_val, str) else raw_val except Exception: value = raw_val self._current_instance.set_attribute(field, value) self._log(f"[OOP] super.{field} = {value}") def handle_SuperConstructorCall(self, node): self._require_instance('super') args_node = node.get('arguments', {}) raw_args = args_node.get('value', '') if isinstance(args_node, dict) else '' args = self._parse_call_parameters({'value': raw_args}) if str(raw_args).strip() else [] parent = self.object_table.class_definitions[self._current_instance.class_name].parent if not parent: raise InterpreterError(f"'{self._current_instance.class_name}' no tiene clase padre.", **self._lc()) self._log(f"[OOP] super.__construct__({args})") ctor = self._find_constructor(parent) if ctor: self._execute_constructor(ctor, self._current_instance, args) else: self._log(f"[OOP] Clase padre '{parent}' no tiene constructor.") # ========================================================================== # Try-Catch-Finally # ========================================================================== def handle_TryCatch(self, node): self._log("[TryCatch] Iniciando bloque try") try_block = node.get('tryBlock') or node.get('block', []) catch_clause = node.get('catchClause', {}) finally_node = node.get('finallyBlock') # El finally puede estar anidado dentro del catchClause if not finally_node and isinstance(catch_clause, dict): finally_node = catch_clause.get('finallyBlock') try: self._exec_try_block(try_block) except ThrowSignal as e: self._log(f"[TryCatch] ThrowSignal capturada: {e}") self._exec_catch_block(catch_clause, e) except InterpreterError as e: self._log(f"[TryCatch] InterpreterError capturado: {e}") self._exec_catch_block(catch_clause, ThrowSignal("RuntimeError", str(e))) except Exception as e: self._log(f"[TryCatch] Excepción Python capturada: {e}") self._exec_catch_block(catch_clause, ThrowSignal("Exception", str(e))) finally: if finally_node: self._log("[TryCatch] Ejecutando finally") self._exec_finally_block(finally_node) self._log("[TryCatch] Bloque try-catch finalizado") def _exec_try_block(self, block): self.symbol_table.push_scope(label="try") if isinstance(block, list): for stmt in block: if self.break_flag or self.return_flag: break self.execute_node(stmt) self.symbol_table.pop_scope(label="try") def _exec_catch_block(self, catch_clause: dict, signal: ThrowSignal): if not catch_clause: return exc_var = catch_clause.get('exception') or catch_clause.get('name') catch_type = catch_clause.get('catchType', {}) expected = catch_type.get('type') if isinstance(catch_type, dict) else None # Filtrar por tipo (catch (e as TipoError)) if expected and expected != signal.exception_class: self._log(f"[TryCatch] Tipo no coincide: esperado '{expected}', recibido '{signal.exception_class}' — re-lanzando") raise signal self.symbol_table.push_scope(label="catch") if exc_var: self.symbol_table.declare(exc_var, Symbol(value=str(signal), declared_type='string')) self._log(f"[TryCatch] Excepción en '{exc_var}': {signal}") block = catch_clause.get('block', []) if isinstance(block, list): for stmt in block: if self.break_flag or self.return_flag: break self.execute_node(stmt) self.symbol_table.pop_scope(label="catch") def _exec_finally_block(self, finally_node: dict): self.symbol_table.push_scope(label="finally") block = finally_node.get('block', []) if isinstance(finally_node, dict) else [] if isinstance(block, list): for stmt in block: if self.break_flag or self.return_flag: break self.execute_node(stmt) self.symbol_table.pop_scope(label="finally") # ========================================================================== # Throw # ========================================================================== def handle_Throw(self, node): exc_class = node.get('exception', 'Error') value_node = node.get('value', {}) message = None if isinstance(value_node, dict): raw = value_node.get('value') if raw: try: message = self.resolve_expression(str(raw)) except: message = str(raw) self._log(f"[Throw] [{exc_class}]: {message}") raise ThrowSignal(exc_class, message) # ========================================================================== # Async / Event Loop # ========================================================================== def _execute_async_call(self, function_name: str, node: dict): """Ejecuta una función async usando asyncio.""" args_node = node.get('arguments', {}) raw_args = args_node.get('value', '') if isinstance(args_node, dict) else '' args = self._parse_call_parameters({'value': raw_args}) if str(raw_args).strip() else [] interp = self async def _coro(): ctx = FunctionContext(interp, function_name, args) return ctx.execute_function() try: loop = asyncio.get_event_loop() if loop.is_running(): self._log(f"[Async] Loop activo → agendando '{function_name}' como tarea") task = asyncio.ensure_future(_coro()) self._async_tasks.append(task) return None # resultado diferido else: self._log(f"[Async] Ejecutando '{function_name}' en loop") return loop.run_until_complete(_coro()) except RuntimeError: return asyncio.run(_coro()) def start_event_loop(self): """Inicia el event loop en un hilo NO demonio.""" if self._event_loop is not None and self._event_loop.is_running(): return self._log("[EventLoop] Iniciando event loop en hilo background (no demonio)") self._event_loop = asyncio.new_event_loop() self._loop_thread = threading.Thread(target=self._run_loop, daemon=False) self._loop_thread.start() def _run_loop(self): asyncio.set_event_loop(self._event_loop) self._event_loop.run_forever() def stop_event_loop(self): """Detiene el event loop.""" if self._event_loop and self._event_loop.is_running(): self._event_loop.call_soon_threadsafe(self._event_loop.stop) self._log("[EventLoop] Señal de parada enviada.") def wait_event_loop(self): """Espera a que el event loop termine (bloquea hasta que se llame a stop_event_loop).""" if not self._event_loop or not self._event_loop.is_running(): self._log("[EventLoop] No hay loop corriendo, nada que esperar.") return self._log("[EventLoop] Esperando a que el event loop termine...") if hasattr(self, '_loop_thread') and self._loop_thread.is_alive(): self._loop_thread.join() self._log("[EventLoop] Event loop terminado.") def schedule_async(self, coro): """Agenda una corrutina en el event loop activo (para UI).""" if self._event_loop and self._event_loop.is_running(): future = asyncio.run_coroutine_threadsafe(coro, self._event_loop) self._async_tasks.append(future) self._log(f"[Async] Corrutina agendada en event loop.") return future raise InterpreterError("Event loop no está activo. Llama start_event_loop() primero.", **self._lc()) # ========================================================================== # Helpers: carga de archivos fuente (.tss) # ========================================================================== @staticmethod def _is_source_file_import(module_str: str) -> bool: """ Detecta si el nodo LibraryCall apunta a un archivo fuente (.tss). El parser puede dejar las comillas o quitarlas; ambos casos se manejan. """ clean = module_str.strip('"\'') return clean.lower().endswith('.tss') @staticmethod def _normalize_file_path(raw: str, base_dir: str) -> str: """ Normaliza una ruta de archivo para Linux, Windows y macOS. • Elimina comillas envolventes • Convierte separadores de Windows (\\ o /) al separador del SO actual • Resuelve rutas relativas contra base_dir • Aplica normpath para eliminar '..', '.' redundantes """ path = raw.strip('"\'') # Unificar separadores: \\ y / → os.sep path = path.replace('\\\\', os.sep).replace('\\', os.sep).replace('/', os.sep) # Rutas relativas → resolver contra el directorio base if not os.path.isabs(path): path = os.path.join(base_dir, path) return os.path.normpath(path) @staticmethod def _derive_module_name(file_path: str) -> str: """ Deriva el nombre de módulo a partir de la ruta del archivo: '/path/to/mycode.tss' → 'mycode' """ print(os.path.splitext(os.path.basename(file_path))[0]) return os.path.splitext(os.path.basename(file_path))[0] def _run_compiler_pipeline(self, file_path: str) -> dict: """ Ejecuta el pipeline completo sobre un archivo .tss: 1. compiler.exe → archivo intermedio (.txt) 2. astAjson.py → JSON Devuelve el AST como diccionario Python. """ COMPILER_EXE_PATH = r"C:\Users\Panch\Escritorio\Tess\compiler.exe" # <- Cambia aquí AST_AJSON_PATH = r"C:\Users\Panch\Escritorio\Tess\astAjson.py" # <- Cambia aquí import subprocess, tempfile, ujson as _json, os # Usa las rutas globales definidas al inicio del archivo compiler = COMPILER_EXE_PATH ast_script = AST_AJSON_PATH # Verifica que existan if not os.path.isfile(compiler): raise InterpreterError(f"No se encuentra compiler.exe en: {compiler}", **self._lc()) if not os.path.isfile(ast_script): raise InterpreterError(f"No se encuentra astAjson.py en: {ast_script}", **self._lc()) fd1, tmp_ast = tempfile.mkstemp(suffix=".txt") fd2, tmp_json = tempfile.mkstemp(suffix=".json") os.close(fd1); os.close(fd2) try: # Etapa 1: compilar .tss → AST intermedio res1 = subprocess.run( [compiler,"-b", file_path, tmp_ast], capture_output=True, text=True ) if res1.returncode != 0: raise InterpreterError( f"Error compilando '{file_path}':\n{res1.stderr.strip()}" , **self._lc()) # Etapa 2: AST intermedio → JSON res2 = subprocess.run( ["python.exe", ast_script, tmp_ast, tmp_json], capture_output=True, text=True ) print(f"Comando ejecutado: python.exe {ast_script} {tmp_ast} {tmp_json}") # input("Presiona enter para continuar") if res2.returncode != 0: raise InterpreterError( f"Error al convertir AST de '{file_path}':\n{res2.stderr.strip()}" , **self._lc()) # Leer y validar JSON if not os.path.isfile(tmp_json): raise InterpreterError(f"No se generó el archivo JSON: {tmp_json}", **self._lc()) if os.path.getsize(tmp_json) == 0: raise InterpreterError(f"El archivo JSON {tmp_json} está vacío.", **self._lc()) with open(tmp_json, "r", encoding="utf-8") as f: try: data = _json.load(f) except _json.JSONDecodeError as e: with open(tmp_json, "r", encoding="utf-8") as f2: content = f2.read() preview = content[:500] + ("..." if len(content) > 500 else "") raise InterpreterError( f"JSON inválido en {tmp_json}:\n{e}\nContenido:\n{preview}" , **self._lc()) return data finally: for p in (tmp_ast, tmp_json): try: os.unlink(p) except OSError: pass def _load_source_file(self, module_raw: str, alias: 'str | None', functions: 'list | None', func_aliases: list, inline_aliases: dict): """ Carga un archivo .tss como módulo: 1. Normaliza la ruta 2. Ejecuta el pipeline compiler → astAjson → JSON 3. Crea un Interpreter aislado y ejecuta el JSON 4. Registra el módulo en _source_modules bajo el nombre efectivo 5. Aplica alias de módulo (as) y alias de miembros (al / use) 6. Si hay 'from ... use', inyecta símbolos en la tabla principal """ import os # 1. Ruta normalizada # Prioridad de búsqueda: # 1. Relativa al archivo fuente actual (donde está el código que hace el import) # 2. Relativa al directorio de trabajo actual # 3. _path_stack (fallback) source_dir = None if self._path_stack: source_dir = self._path_stack[-1] # Intentar resolver la ruta en orden de prioridad raw_stripped = module_raw.strip('"\'') candidates = [] if source_dir: candidates.append(self._normalize_file_path(module_raw, source_dir)) candidates.append(self._normalize_file_path(module_raw, os.getcwd())) file_path = None for candidate in candidates: if os.path.isfile(candidate): file_path = candidate break if file_path is None: searched = ', '.join(f"'{c}'" for c in candidates) raise InterpreterError( f"No se encontró el archivo de módulo '{raw_stripped}'. " f"Rutas buscadas: {searched}" , **self._lc()) # Nombre de módulo: alias de módulo > nombre derivado del archivo default_name = self._derive_module_name(file_path) module_name = alias if alias else default_name self._log(f"Cargando módulo fuente '{module_name}' desde '{file_path}'") # 2. Si ya está cargado con este nombre, no reejecutar if module_name not in self._source_modules: # Pipeline: .tss → JSON AST ast_data = self._run_compiler_pipeline(file_path) # 3. Intérprete aislado (scope propio) iso = Interpreter(debug_mode=self.debug_mode) iso._base_dir = os.path.dirname(file_path) iso.interpret(ast_data, source_path=file_path) # 4. Registrar el proxy proxy = SourceModuleProxy(iso, module_name) self._source_modules[module_name] = proxy self._log(f"Módulo fuente '{module_name}' ejecutado y registrado.") else: proxy = self._source_modules[module_name] # 5a. Aliases de miembros (from … use x al myX) # inline_aliases: {original_name: alias_name} for orig, mem_alias in inline_aliases.items(): proxy.apply_member_alias(orig, mem_alias) # 5b. Aliases posicionales (from … use x, y al a, b) if functions and func_aliases: for orig, mem_alias in zip(functions, func_aliases): proxy.apply_member_alias(orig, mem_alias) # 6. "from … use …": inyectar símbolos específicos en la tabla principal if functions: self._inject_source_symbols(proxy, functions, inline_aliases, func_aliases) self._log(f"Módulo fuente '{module_name}' listo.") def _inject_source_symbols(self, proxy: SourceModuleProxy, functions: list, inline_aliases: dict, func_aliases: list): """ Para 'from "file.tss" use x, y al myY': Inyecta los símbolos solicitados directamente en la tabla principal. Resolución de nombre expuesto (prioridad): 1. inline_aliases[original] → "x al myX" 2. func_aliases[i] → "use x, y al a, b" 3. original_name → sin alias """ pos_alias = {orig: fa for orig, fa in zip(functions, func_aliases)} if func_aliases else {} for orig_name in functions: # Nombre que tendrá en la tabla principal exposed = (inline_aliases.get(orig_name) or pos_alias.get(orig_name) or orig_name) iso = proxy._isolated_interpreter # ¿Es función? try: func_node = iso.symbol_table.get_function(orig_name) func_params = iso.symbol_table.get_function_params(orig_name) # Declarar función con su nombre expuesto en la tabla principal self.symbol_table.declare_function(exposed, func_node) self.symbol_table.declare_function_params(exposed, func_params) self._log(f" Importada función '{orig_name}' como '{exposed}'") continue except UndeclaredVariableError: pass # ¿Es variable/constante? try: sym = iso.symbol_table.get_symbol(orig_name) import copy new_sym = Symbol(value=sym.value, declared_type=sym.declared_type, is_const=sym.is_const) self.symbol_table.declare(exposed, new_sym) self._log(f" Importada variable '{orig_name}' como '{exposed}'") continue except UndeclaredVariableError: pass raise InterpreterError( f"El símbolo '{orig_name}' no existe en el módulo fuente." , **self._lc()) def _evaluate_parameter_value(self, value_str): """Evalúa el valor de un parámetro (puede ser variable, expresión o literal)""" # Si ya es un valor Python nativo (int, float, bool, list, etc.), devolverlo directo if not isinstance(value_str, str): return value_str value_str = value_str.strip() # ── Regla fundamental: entrecomillado = string literal, sin comillas = variable/expresión if (value_str.startswith('"') and value_str.endswith('"')) or (value_str.startswith("'") and value_str.endswith("'")): # AGREGADO: los argumentos de tipo string también admiten # interpolación #(...) — ej. read(x, msg="valor actual #(x): ") # antes se devolvía el texto crudo sin resolver el #(x). return self._interpolate_hash_string(value_str[1:-1]) # Booleanos y null — antes de intentar como variable if value_str.lower() == 'true': return True if value_str.lower() == 'false': return False if value_str.lower() == 'null': return None # Literales numéricos — antes de intentar como variable try: return int(value_str) except ValueError: pass try: return float(value_str) except ValueError: pass # Array literal [...] if value_str.startswith('[') and value_str.endswith(']'): inner = value_str[1:-1].strip() if not inner: return [] items = self._split_args_respecting_brackets(inner) return [self._evaluate_parameter_value(i.strip()) for i in items] try: # Llamada a función if self._is_function_call(value_str): return self._execute_function_call_from_string(value_str) # Expresión con operaciones (pero no arrays ni strings con punto) if any(op in value_str for op in ['+', '-', '*', '/', '%']) and \ not (value_str.startswith('"') and value_str.endswith('"')): return self.resolve_expression(value_str) # Acceso a módulo/instancia con punto: mod.VALOR o inst.metodo() if '.' in value_str and not value_str.replace('.', '', 1).isdigit(): return self.resolve_expression(value_str) # Variable return self.symbol_table.get_value(value_str) except TesseractError: raise # NameError, DivisionByZeroError, etc. — no silenciar except UndeclaredVariableError: # Aquí solo llegan identificadores sin comillas que no existen como variable raise NameError( msg("name.undefined", name=value_str), **self._lc_here(value_str) ) def handle_ParameterAsignement(self, node): if not hasattr(self, '_current_function') or not self._current_function: self._log("ADVERTENCIA: ParameterAsignement fuera de función") return param_name = node["name"] value_node = node.get("value", {}) # Evaluar valor if "operation" in value_node: operation_node = value_node["operation"] expression_str = operation_node.get("value") if isinstance(operation_node, dict) else operation_node final_value = self.resolve_expression(expression_str) else: raw_value = value_node.get("value") if isinstance(raw_value, str) and not (raw_value.startswith('"') and raw_value.endswith('"')): try: final_value = self.symbol_table.get_value(raw_value) except UndeclaredVariableError: final_value = raw_value else: final_value = raw_value # ASIGNAR A TABLA ESPECÍFICA DE PARÁMETROS self.symbol_table.declare_function_param_value( self._current_function, param_name, final_value ) self._log(f" Asignado parámetro '{param_name}': {final_value}") def handle_if_Condition(self, node): self._log("Iniciando bloque If-Condition.") # 1. Comprueba la condición principal del IF if self.evaluate_expression(node["condition"]): self._log(" Condición 'if' es VERDADERA. Ejecutando su bloque.") if "block" in node: self.symbol_table.push_scope(label="if") for statement in node["block"]: if self.break_flag or self.return_flag: break self.execute_node(statement) if self.return_flag: self.symbol_table.pop_scope(label="if") return self.symbol_table.pop_scope(label="if") self._log("Finalizado bloque if-Condition.") return # 2. Procesar elseIf si existe current_node = node while "elseIf" in current_node: else_if_node = current_node["elseIf"] condition = else_if_node.get("condition") or else_if_node.get("value") if condition and self.evaluate_expression(condition): self._log(f" Condición 'elseIf' ({condition}) es VERDADERA. Ejecutando su bloque.") self.symbol_table.push_scope(label="elseIf") if "block" in else_if_node: for statement in else_if_node["block"]: if self.break_flag or self.return_flag: break self.execute_node(statement) if self.return_flag: self.symbol_table.pop_scope(label="elseIf") return self.symbol_table.pop_scope(label="elseIf") self._log("Finalizado bloque if-Condition.") return # Moverse al siguiente nivel de anidación current_node = else_if_node # 3. Procesar else si existe if "else" in node: self._log(" Ninguna condición anterior fue verdadera. Ejecutando bloque 'else'.") else_node = node["else"] if "block" in else_node: self.symbol_table.push_scope(label="else") for statement in else_node["block"]: if self.break_flag or self.return_flag: break self.execute_node(statement) if self.return_flag: self.symbol_table.pop_scope(label="else") return self.symbol_table.pop_scope(label="else") self._log("Finalizado bloque if-Condition.") def handle_ForLoop(self, node): """ Maneja todas las variantes del for: ESTILO C: for(int i = 0; i < 10; i++) → iterator.value = "int i = 0; i<10; i++" for(var i = 0; i < n; i += 2) → id. MODERNO (for-in): for(var i in 1..10) → variable="var i", iterator="1..10" for(var i in arr) → itera array/tupla/dict(valores)/rango for(var i in dict.keys()) → itera claves del dict for(var i in x >= 10) → condicional (infinito detectado → error) for(var i in i < 10) → condicional con propia variable MULTI-VARIABLE: for(var i, var z in 1..10; arr) → variable="var i, var z", iterator="1..10; arr" """ variable_str = node.get("variable", "") iterator_node = node.get("iterator", {}) iterator_str = iterator_node.get("value", "") if isinstance(iterator_node, dict) else str(iterator_node) block = node.get("block", []) self._log(f"[FOR] variable='{variable_str}' iterator='{iterator_str}'") # ── Detectar estilo ────────────────────────────────────────────────── is_c_style = self._for_is_c_style(iterator_str) if is_c_style: self._for_execute_c_style(iterator_str, block) else: # Parsear declaraciones de variable(s) # Soporta: "var i = 0 in cond" → extraer init del variable_str si contiene 'in' actual_var_str = variable_str var_defs = self._for_parse_var_defs(actual_var_str) iter_parts = self._for_split_iterators(iterator_str) if len(var_defs) == 1: self._for_execute_modern(var_defs[0], iter_parts[0] if iter_parts else '', block) else: # Multi-variable: exactamente 2 iter1 = iter_parts[0] if len(iter_parts) > 0 else '' iter2 = iter_parts[1] if len(iter_parts) > 1 else '' self._for_execute_multi(var_defs[0], var_defs[1], iter1, iter2, block) # ───────────────────────────────────────────────────────────────────────── # FOR helpers # ───────────────────────────────────────────────────────────────────────── def _for_is_c_style(self, iterator_str: str) -> bool: """ Heurística: es estilo C si el iterator tiene exactamente 2 ';' y el primero contiene '=' (inicialización). Ej: "int i = 0; i<10; i++" """ parts = [p.strip() for p in iterator_str.split(';')] if len(parts) != 3: return False init = parts[0] return '=' in init or re.search(r'\bvar\b|\bint\b|\bfloat\b|\bstring\b|\bbool\b', init) def _for_parse_var_defs(self, variable_str: str) -> list: """ Parsea 'var i', 'var i, var z', 'int i = 0', 'c, var z' etc. Retorna lista de dicts: [{'name': 'i', 'declare': True, 'type': 'dynamic', 'init': None}, ...] """ result = [] parts = [p.strip() for p in variable_str.split(',')] for part in parts: d = self._for_parse_single_var(part.strip()) result.append(d) return result if result else [{'name': variable_str.strip(), 'declare': False, 'type': 'dynamic', 'init': None}] def _for_parse_single_var(self, s: str) -> dict: """ Parsea una declaración de variable de for: 'var i' → declare=True, name='i', type='dynamic', init=None 'var i = 5' → declare=True, name='i', type='dynamic', init=5 'int i = 0' → declare=True, name='i', type='int', init=0 'int i' → declare=True, name='i', type='int', init=0 'i' → declare=False (buscar en tabla de símbolos) 'x = 0' → declare=False, name='x', init=0 """ s = s.strip() # Con tipo/var explícito (con o sin init) m = re.match(r'^(var|int|float|string|bool|array|tuple|dict|range|dynamic|any)\s+' r'([A-Za-z_]\w*)\s*(?:=\s*(.+))?$', s) if m: kw, name, init_str = m.group(1), m.group(2), m.group(3) typ = 'dynamic' if kw == 'var' else kw if init_str: try: init = self.resolve_expression(init_str.strip()) except Exception: init = None else: # Default por tipo defaults = {'int': 0, 'float': 0.0, 'bool': False, 'string': '', 'dynamic': None, 'any': None} init = defaults.get(typ, None) return {'name': name, 'declare': True, 'type': typ, 'init': init} # Sin tipo pero con asignación: "x = 0" m2 = re.match(r'^([A-Za-z_]\w*)\s*=\s*(.+)$', s) if m2: name, init_str = m2.group(1), m2.group(2) try: init = self.resolve_expression(init_str.strip()) except Exception: init = None return {'name': name, 'declare': False, 'type': 'dynamic', 'init': init} # Solo nombre → buscar en tabla de símbolos if re.fullmatch(r'[A-Za-z_]\w*', s): return {'name': s, 'declare': False, 'type': 'dynamic', 'init': None} return {'name': s, 'declare': False, 'type': 'dynamic', 'init': None} def _for_split_iterators(self, iterator_str: str) -> list: """Divide por ';' los iteradores del for moderno/multi.""" return [p.strip() for p in iterator_str.split(';') if p.strip()] def _for_resolve_iterable(self, iter_str: str): """ Convierte el string del iterador en una lista iterable de Python. Soporta: rango, array, tupla, dict, variable, condición, NULL. Retorna (iterable_list, is_conditional, condition_str) """ s = iter_str.strip() # NULL / null → vacío if s.upper() == 'NULL': return [], False, None # Rango (incluyendo half-open) if '..' in s: rng = self._try_parse_range(s) if rng is not None: return rng.expand(), False, None # Condición: contiene operadores de comparación pero no es literal COND_OPS = ('>=', '<=', '!=', '==', '>', '<') has_cond = any(op in s for op in COND_OPS) and not s.startswith('[') and \ not s.startswith('(') and not s.startswith('{') and \ not s.startswith('"') and not s.startswith("'") if has_cond: return None, True, s # Literal o variable try: val = self.resolve_expression(s) except Exception: val = None if val is None: return [], False, None if isinstance(val, RangeValue): return val.expand(), False, None if isinstance(val, (list, tuple)): return list(val), False, None if isinstance(val, dict): return list(val.values()), False, None # por defecto: valores # Escalar → no iterable return [], False, None def _evaluate_condition(self, cond_str: str) -> bool: """ Evalúa una condición de bucle. Maneja: 'true'/'false' literales, y delega al evaluador de expresiones. """ s = cond_str.strip().lower() if s in ('true', '1'): return True if s in ('false', '0', 'null'): return False try: return bool(self.evaluate_expression(cond_str)) except TesseractError: raise except Exception: try: return bool(self.resolve_expression(cond_str)) except TesseractError: raise except Exception: return False def _for_check_infinite(self, cond_str, var_name, block=None): return self._detect_infinite_loop(cond_str, block or [], var_name) def _detect_infinite_loop(self, cond_str, block, iter_var=""): cond_lower = cond_str.strip().lower() if cond_lower in ("false", "0", "null"): return False cond_vars = set(re.findall(r"[A-Za-z_]\w*", cond_str)) for kw in ("true","false","null","and","or","not","if","else"): cond_vars.discard(kw) has_exit, mod_vars = self._block_analysis(block or []) if has_exit: return False # La variable de iteración es modificada externamente por el for (auto-increment) if iter_var: mod_vars.add(iter_var) if cond_lower in ("true", "1"): return True if cond_vars and not (cond_vars & mod_vars): return True return False def _block_analysis(self, block): """ Analiza un bloque estáticamente. Retorna (has_exit: bool, modified_vars: set) Break / return → has_exit = True i++/i-- / i+=n / i=... → mod_vars.add(var) Recursivo en cualquier sub-bloque o statements. """ has_exit = False mod_vars = set() # Normalizar el bloque a lista de nodos if isinstance(block, list): nodes = block elif isinstance(block, dict): # Puede ser un dict con clave "statements", "block", etc. # cuyo valor es la lista real de nodos inner = None for key in ('statements', 'block', 'body', 'nodes'): if key in block and isinstance(block[key], list): inner = block[key] break if inner is not None: nodes = inner else: # Es un nodo único con tipo como clave nodes = [block] else: return False, set() for node in nodes: if not isinstance(node, dict): continue ntype = list(node.keys())[0] content = node.get(ntype, {}) # content puede ser lista (ej: statements) — la procesamos recursivo if isinstance(content, list): sub_e, sub_v = self._block_analysis(content) if sub_e: has_exit = True mod_vars |= sub_v continue if not isinstance(content, dict): content = {} # ── Break / Return ───────────────────────────────────────────── if ntype == 'CallExpression': fn = content.get('function', '') if fn in ('Break', 'Return', 'break', 'return'): has_exit = True continue if ntype in ('ReturnStatement', 'BreakStatement', 'Return', 'Break'): has_exit = True continue # ── Asignaciones ─────────────────────────────────────────────── if ntype in ('VariableAsignement', 'ModuleAsignement', 'Assignment', 'VariableAssignment'): name = content.get('name', '') if name: mod_vars.add(name.split('.')[0]) # ── Asignación compuesta: i += n (value puede ser "i+=1") ───── if ntype == 'CompoundAssignment': name = content.get('name', '') or content.get('variable', '') if name: mod_vars.add(name.split('.')[0]) # ── Incremento/decremento: i++ / i-- / ++i / --i ────────────── # Nombres exactos del AST (igual que los handlers: handle_PostIncrementStatement) if ntype in ('PostIncrementStatement', 'PostDecrementStatement', 'PreIncrementStatement', 'PreDecrementStatement', 'IncrementDecrement', 'IncrementStatement', 'DecrementStatement', 'Increment', 'Decrement', 'postIncrementStatement', 'postDecrementStatement', 'preIncrementStatement', 'preDecrementStatement'): name = content.get('value', '') or content.get('name', '') or content.get('variable', '') if name: mod_vars.add(str(name)) # ── Recursión en sub-bloques ─────────────────────────────────── for key in ('block', 'body', 'thenBlock', 'elseBlock', 'ifBlock', 'Block', 'then', 'else', 'statements', 'nodes', 'consequent', 'alternate', 'cases'): sub = content.get(key) if sub: sub_e, sub_v = self._block_analysis(sub) if sub_e: has_exit = True mod_vars |= sub_v return has_exit, mod_vars def _for_run_block(self, block): """Ejecuta el bloque del for. Retorna True si se debe salir (break/return).""" if isinstance(block, list): self.symbol_table.push_scope(label="for-body") for stmt in block: if self.break_flag or self.return_flag: break self.execute_node(stmt) self.symbol_table.pop_scope(label="for-body") elif isinstance(block, dict): self.execute_block(block) return self.break_flag or self.return_flag def _for_set_var(self, vdef: dict, value): """Asigna el valor a la variable de iteración respetando tipo si está declarado.""" name = vdef['name'] typ = vdef.get('type', 'dynamic') actual = _get_value_type(value) if typ not in ('dynamic', 'any') and actual not in ('dynamic', 'null', 'any'): # Permisión int→float ok = (typ == actual) or (typ == 'float' and actual == 'int') if not ok: raise InterpreterError( f"[FOR] Error de tipo en iteración: variable '{name}' " f"declarada como '{typ}' pero recibió '{actual}' ({value!r})", **self._lc()) try: self.symbol_table.set_value(name, value) except UndeclaredVariableError: self.symbol_table.declare(name, Symbol(value=value, declared_type=typ)) MAX_FOR_ITERATIONS = 100_000 # seguridad anti-infinito def _for_execute_c_style(self, iterator_str: str, block): """ Estilo C: "int i = 0; i<10; i++" Partes: [init, condition, update] """ parts = [p.strip() for p in iterator_str.split(';')] if len(parts) != 3: raise InterpreterError(f"[FOR] Estilo C mal formado: '{iterator_str}'", **self._lc()) init_str, cond_str, upd_str = parts self.symbol_table.push_scope(label="for-c") # Inicialización self._for_c_init(init_str) # Extraer variables modificadas por el update (i++, i+=2, i=i+1, etc.) upd_vars = set(re.findall(r'[A-Za-z_]\w*', upd_str.split('=')[0].split('+')[0].split('-')[0].strip())) # Detectar posible bucle infinito — el update modifica vars implícitamente has_exit, block_mod_vars = self._block_analysis(block) all_mod_vars = block_mod_vars | upd_vars cond_vars = set(re.findall(r'[A-Za-z_]\w*', cond_str)) cond_lower = cond_str.strip().lower() if (not has_exit and not (cond_vars & all_mod_vars) and cond_lower not in ('false', '0', 'null')): self.symbol_table.pop_scope(label="for-c") raise InterpreterError( f"[FOR] Bucle infinito detectado (condición: '{cond_str}'). " f"Usa 'break' o 'return' para salir, o asegura que el update " f"modifique las variables de la condición.", **self._lc()) n = 0 while True: # Evaluar condición try: cond_val = self._evaluate_condition(cond_str) except Exception as e: self.symbol_table.pop_scope(label="for-c") raise InterpreterError(f"[FOR] Error evaluando condición '{cond_str}': {e}", **self._lc()) if not cond_val: break n += 1 if False: # sin límite de iteraciones self.symbol_table.pop_scope(label="for-c") raise InterpreterError( f"[FOR] Bucle excedió {self.MAX_FOR_ITERATIONS} iteraciones. " f"Usa 'break' o 'return' para salir de bucles largos.", **self._lc()) if self._for_run_block(block): break if self.return_flag: break # Actualización try: self._for_c_update(upd_str) except Exception as e: self.symbol_table.pop_scope(label="for-c") raise InterpreterError(f"[FOR] Error en actualización '{upd_str}': {e}", **self._lc()) self.break_flag = False self.symbol_table.pop_scope(label="for-c") self._log(f"[FOR] Estilo C finalizado tras {n} iteraciones.") def _for_c_init(self, init_str: str): """Ejecuta la inicialización del for C: 'int i = 0' o 'i = 0'.""" init_str = init_str.strip() m = re.match( r'^(?:(?:var|int|float|string|bool|array|tuple|dict|dynamic|any)\s+)?' r'([A-Za-z_]\w*)\s*=\s*(.+)$', init_str) if m: name, val_str = m.group(1), m.group(2) val = self.resolve_expression(val_str.strip()) # Detectar tipo type_m = re.match(r'^(var|int|float|string|bool|array|tuple|dict|dynamic|any)\s+', init_str) typ = 'dynamic' if not type_m or type_m.group(1) == 'var' else type_m.group(1) self.symbol_table.declare(name, Symbol(value=val, declared_type=typ)) else: # Puede ser solo "i++" o similar, ignorar pass def _for_c_update(self, upd_str: str): """Ejecuta la actualización del for C: 'i++', 'i--', 'i += 2', 'i = i + 1'.""" upd_str = upd_str.strip() def _set(n, v): try: self.symbol_table.set_value(n, v) except (ConstError, NameError) as _e: _lc_err = self._lc_here(n) _e.line = _e.line or _lc_err.get("line") _e.col = _e.col or _lc_err.get("col") _e.file = _e.file if _e.file != "" else self._source_file _e.function = _e.function or self._current_function raise # i++ m = re.fullmatch(r'([A-Za-z_]\w*)\s*\+\+', upd_str) if m: n = m.group(1); v = self.symbol_table.get_value(n) _set(n, v + 1); return # i-- m = re.fullmatch(r'([A-Za-z_]\w*)\s*--', upd_str) if m: n = m.group(1); v = self.symbol_table.get_value(n) _set(n, v - 1); return # i += expr m = re.fullmatch(r'([A-Za-z_]\w*)\s*\+=\s*(.+)', upd_str) if m: n, expr = m.group(1), m.group(2) v = self.symbol_table.get_value(n) _set(n, v + self.resolve_expression(expr)); return # i -= expr m = re.fullmatch(r'([A-Za-z_]\w*)\s*-=\s*(.+)', upd_str) if m: n, expr = m.group(1), m.group(2) v = self.symbol_table.get_value(n) _set(n, v - self.resolve_expression(expr)); return # i *= expr m = re.fullmatch(r'([A-Za-z_]\w*)\s*\*=\s*(.+)', upd_str) if m: n, expr = m.group(1), m.group(2) v = self.symbol_table.get_value(n) _set(n, v * self.resolve_expression(expr)); return # i = expr m = re.fullmatch(r'([A-Za-z_]\w*)\s*=\s*(.+)', upd_str) if m: n, expr = m.group(1), m.group(2) _set(n, self.resolve_expression(expr)); return # Desconocido: intentar como expresión try: self.resolve_expression(upd_str) except Exception: pass def _for_execute_modern(self, vdef: dict, iter_str: str, block): """ For moderno de una variable: for(var i in 1..10) / for(var i in arr) etc. """ name = vdef['name'] declare = vdef['declare'] typ = vdef.get('type', 'dynamic') init_val = vdef.get('init') self.symbol_table.push_scope(label="for-modern") # ── REPARADO ───────────────────────────────────────────────────────── # Antes, el iterador se resolvía (self._for_resolve_iterable) ANTES de # declarar/asignar la variable de control en el scope. Eso rompía # rangos semi-abiertos que se auto-referencian con su propia variable # de control, por ejemplo: # for(var i = 0 in i..n-1) # Aquí "i" aparece en el lado izquierdo del propio rango: al intentar # resolverlo, la variable "i" todavía no existía en la tabla de # símbolos, así que _try_parse_range fallaba en silencio (raw_s=None) # y el for terminaba tratando el iterador como si no fuera ni rango # ni condición, cayendo en una lista vacía → el bucle iteraba 0 veces # sin ningún error visible (el 'ord_burbuja' del debug se veía vacío). # # La reparación es declarar/asignar la variable de control PRIMERO # (con su init_val explícito, ej. 0 en 'var i = 0'), y sólo DESPUÉS # resolver el iterador. Así "i..n-1" se resuelve como un RANGO real # (i=0 ya visible), no como condición ni como iterable vacío. no_init_explicito = init_val is None if declare: self.symbol_table.declare(name, Symbol(value=init_val, declared_type=typ)) else: # Buscar variable existente try: sym = self.symbol_table.get_symbol(name) # Si tenía init_val por default, actualizarla if init_val is not None and sym.value is None: self.symbol_table.set_value(name, init_val) except UndeclaredVariableError: self.symbol_table.declare(name, Symbol(value=init_val, declared_type=typ)) # Ahora sí: con la variable de control ya visible en el scope, # resolver el iterador. Esto soporta rangos auto-referenciados como # 'i..n-1' (deben tratarse como RANGO, no como condición), además de # las formas condicionales, arrays, dicts, tuplas, etc. de siempre. iterable, is_cond, cond_str = self._for_resolve_iterable(iter_str) # Si resultó ser una forma condicional (ej. for(var i in i < 10)) y # no había init explícito, completar el valor por defecto ahora que # ya sabemos que es condicional (mismo comportamiento de antes, sólo # que se aplica después de conocer is_cond en vez de antes). if no_init_explicito and is_cond: try: existing = self.symbol_table.get_value(name) except UndeclaredVariableError: existing = None if existing is None: existing = 0 if typ in ('int', 'float', 'dynamic', 'any') else None self.symbol_table.set_value(name, existing) if is_cond: # Forma condicional: for(var i in i < 10) # Auto-incrementa la variable de iteración tras cada iteración del cuerpo. if self._for_check_infinite(cond_str, name, block): self.symbol_table.pop_scope(label="for-modern") raise InterpreterError( f"[FOR] Bucle potencialmente infinito (condición: '{cond_str}'). " f"Usa 'break' o 'return'.", **self._lc()) n = 0 while True: try: cond_val = self._evaluate_condition(cond_str) except Exception as e: self.symbol_table.pop_scope(label="for-modern") raise InterpreterError(f"[FOR] Error en condición '{cond_str}': {e}", **self._lc()) if not cond_val: break n += 1 if False: # sin límite de iteraciones self.symbol_table.pop_scope(label="for-modern") raise InterpreterError( f"[FOR] Bucle excedió {self.MAX_FOR_ITERATIONS} iteraciones. " f"Usa 'break' o 'return'.", **self._lc()) if self._for_run_block(block): break if self.return_flag: break # Auto-incrementar la variable de iteración self._for_auto_increment(name, typ) else: # Iteración sobre colección / rango for item in iterable: try: self._for_set_var(vdef, item) except InterpreterError as e: # Error de tipo → detener con mensaje self.symbol_table.pop_scope(label="for-modern") print(f"[FOR] Iteración detenida: {e}") self.break_flag = False return self._log(f"[FOR] {name} = {item!r}") if self._for_run_block(block): break if self.return_flag: break self.break_flag = False self.symbol_table.pop_scope(label="for-modern") def _for_auto_increment(self, name: str, typ: str): """ Auto-incrementa la variable de iteración condicional: int/float/dynamic → +1 string → siguiente carácter unicode Si la variable no existe o no es incrementable, no hace nada. """ try: val = self.symbol_table.get_value(name) except UndeclaredVariableError: return try: if isinstance(val, int): self.symbol_table.set_value(name, val + 1) elif isinstance(val, float): import decimal as _dec new_val = float(_dec.Decimal(str(val)) + _dec.Decimal('0.1')) self.symbol_table.set_value(name, new_val) elif isinstance(val, str) and len(val) == 1: self.symbol_table.set_value(name, chr(ord(val) + 1)) # bool, array, dict, etc → no auto-increment, el cuerpo debe manejarlo except Exception: pass def _for_execute_multi(self, vdef1: dict, vdef2: dict, iter1_str: str, iter2_str: str, block): """ Multi-variable: for(var i, var z in 1..10; arr) La primera iteración decide cuándo acaba. """ self.symbol_table.push_scope(label="for-multi") # Declarar variables for vd in (vdef1, vdef2): name = vd['name']; typ = vd.get('type', 'dynamic') init = vd.get('init') if vd['declare']: self.symbol_table.declare(name, Symbol(value=init, declared_type=typ)) else: try: self.symbol_table.get_symbol(name) except UndeclaredVariableError: self.symbol_table.declare(name, Symbol(value=None, declared_type=typ)) iterable1, _, _ = self._for_resolve_iterable(iter1_str) iterable2, _, _ = self._for_resolve_iterable(iter2_str) iter2_gen = iter(iterable2) for item1 in iterable1: try: self._for_set_var(vdef1, item1) except InterpreterError as e: print(f"[FOR] Iteración detenida en var 1: {e}") break # Intentar avanzar el segundo iterador try: item2 = next(iter2_gen) try: self._for_set_var(vdef2, item2) except InterpreterError as e: print(f"[FOR] Iteración detenida en var 2: {e}") break except StopIteration: # El segundo iterador terminó, seguimos con el primero # pero la variable 2 conserva su último valor pass self._log(f"[FOR-MULTI] {vdef1['name']}={item1!r}, " f"{vdef2['name']}={self.symbol_table.get_value(vdef2['name']) if True else '?'}") if self._for_run_block(block): break if self.return_flag: break self.break_flag = False self.symbol_table.pop_scope(label="for-multi") def handle_WhileLoop(self, node, block_node=None): """Maneja WhileLoop con bloque integrado""" condition_str = node["condition"] self._log(f"Iniciando bucle 'while' con condición: {condition_str}") self.break_flag = False if "block" in node: block_content = node["block"] # ── Detección de bucle infinito ─────────────────────────────────── if self._detect_infinite_loop(condition_str, block_content): raise RuntimeError( f"Bucle 'while' infinito detectado. " f"Condición '{condition_str}' nunca cambia: " f"ninguna variable de la condición se modifica en el bloque, " f"y no hay 'break' ni 'return'.", **self._lc() ) while self._evaluate_condition(condition_str): self._log(" Condición 'while' es VERDADERA. Ejecutando bloque.") # Ejecutar el bloque integrado if isinstance(block_content, list): self.symbol_table.push_scope(label="while") for statement in block_content: if self.break_flag or self.return_flag: break self.execute_node(statement) if self.return_flag: self.symbol_table.pop_scope(label="while") return self.symbol_table.pop_scope(label="while") elif isinstance(block_content, dict): self.execute_block(block_content) if self.break_flag: self._log(" Instrucción 'Break' detectada. Saliendo del bucle 'while'.") break if self.return_flag: self._log(" Instrucción 'Return' detectada. Saliendo del bucle 'while'.") break else: self._log(" Advertencia: WhileLoop sin bloque de código") self.break_flag = False self._log(f"Finalizado bucle 'while'.") def handle_unknown(self, node_content): print(f"ADVERTENCIA CRÍTICA: No hay manejador para este tipo de nodo. Contenido: {node_content}") def _handle_increment_decrement(self, node, amount): var_name = node['value'] op_type = "incremento" if amount > 0 else "decremento" try: symbol = self.symbol_table.get_symbol(var_name) declared_type = symbol.declared_type current_value = symbol.value # ========================================================== # LÓGICA PARA VARIABLES ESTÁTICAS # ========================================================== if declared_type != 'dynamic': if declared_type in ['int', 'float']: # Es un tipo estático numérico, la operación es válida. # Hacemos una comprobación extra por si el valor actual no es un número. if not isinstance(current_value, (int, float)): self._log(f"ERROR: La variable estática '{var_name}' (tipo {declared_type}) contiene un valor no numérico: '{current_value}'.") return # Éxito para estáticas numéricas new_value = current_value + amount self.symbol_table.set_value(var_name, new_value) self._log(f" Operación de {op_type}: '{var_name}' ahora es {new_value}") else: # Es un tipo estático NO numérico (string, boolean, etc.). Error. self._log(f"ERROR: No se puede aplicar {op_type} a la variable '{var_name}' porque su tipo estático es '{declared_type}'.") return # ========================================================== # LÓGICA PARA VARIABLES DINÁMICAS # ========================================================== else: start_value = None # Si es NULL (None en Python), se permite y empieza en 0. if current_value is None: start_value = 0 # Si es un número, se permite. elif isinstance(current_value, (int, float)): start_value = current_value # Si start_value tiene un valor, la operación es válida. if start_value is not None: new_value = start_value + amount self.symbol_table.set_value(var_name, new_value) self._log(f" Operación de {op_type}: '{var_name}' ahora es {new_value}") else: # Si no, es porque el valor es string, boolean, etc. Advertencia. self._log(f"ADVERTENCIA: No se puede aplicar {op_type} a la variable dinámica '{var_name}'. Su valor actual ('{current_value}') no es válido para esta operación.") return except UndeclaredVariableError as e: self._log(e) def handle_PostIncrementStatement(self, node): self._handle_increment_decrement(node, 1) def handle_PostDecrementStatement(self, node): self._handle_increment_decrement(node, -1) def handle_PreIncrementStatement(self, node): self._handle_increment_decrement(node, 1) def handle_PreDecrementStatement(self, node): self._handle_increment_decrement(node, -1) def _evaluate_concatenated_string(self, arg_str): """ Evalúa cadenas con variables concatenadas: "hola " . var . p1.name El separador es ' . ' (punto rodeado de espacios) para no confundir con accesos de struct/módulo (p1.name) ni con floats (3.14). """ # Guard: número puro stripped = arg_str.strip() try: return int(stripped) except (ValueError, AttributeError): pass try: if '.' in stripped and '..' not in stripped: return float(stripped) except (ValueError, AttributeError): pass # Dividir por ' . ' (punto con espacios) respetando strings parts = self._split_concat_parts(arg_str) # --------------------------------------------------------- # FIX: GUARD PARA EVITAR RECURSIÓN INFINITA # Si la división devuelve exactamente 1 elemento y es igual # a la entrada original, NO hubo concatenación. # --------------------------------------------------------- if len(parts) == 1 and parts[0] == arg_str: return _UNRESOLVED result = [] for part in parts: part = part.strip() if not part: continue val = self.resolve_expression(part) if isinstance(val, StructInstance): # Si la expresión resolvió a StructInstance completo sin campo → error result.append(val.format_print(part)) else: result.append(_fmt_val(val)) return ''.join(result) def _split_concat_parts(self, s: str) -> list: """ Divide por ' . ' (punto con al menos un espacio en algún lado) respetando strings entre comillas. No parte por puntos sin espacios (p1.name, 3.14, arr.length). Ejemplos: '"hola" . p1.name . x' → ['"hola"', 'p1.name', 'x'] '"a" . "b"' → ['"a"', '"b"'] 'p1.name' → ['p1.name'] (sin espacios, no parte) """ parts = [] buf = [] in_str = False str_ch = '"' i = 0 while i < len(s): ch = s[i] if in_str: buf.append(ch) if ch == str_ch: in_str = False i += 1 continue if ch in ('"', "'"): in_str = True str_ch = ch buf.append(ch) i += 1 continue # Detectar ' . ' — punto con espacio en algún lado, # O punto entre cierre de string y siguiente token if ch == '.': before = buf[-1] if buf else '' after = s[i+1] if i+1 < len(s) else '' # Es separador si: # 1. Hay espacio antes o después del punto # 2. El carácter anterior es cierre de string ('" o ') is_sep = (before in (' ', '"', "'") or after == ' ') if is_sep: if buf and buf[-1] == ' ': buf.pop() parts.append(''.join(buf)) buf = [] if after == ' ': i += 2 else: i += 1 continue buf.append(ch) i += 1 if buf: parts.append(''.join(buf)) return [p for p in parts if p.strip()] def _execute_type_method_chain(self, var_name: str, chain_str: str, origin_value, is_const: bool = False, param_frame: str = None): """ Ejecuta una cadena de métodos/atributos sobre un valor de tipo primitivo. Soporta: - Métodos con args: x.toUpperCase() x.slice(0,3) - Atributos: x.length x.type - .mut al final: x.toUpperCase().mut - Encadenamiento: x.trim().toUpperCase().slice(0,3).mut param_frame: AGREGADO. Si 'var_name' es un PARÁMETRO de función (no una variable normal de symbol_table), se debe pasar aquí el frame_key de la función dueña de ese parámetro (ver function_param_values). Sin esto, 'push'/'pop'/etc. ('mutable_default') y '.mut' intentaban persistir con symbol_table.set_value(var_name, ...), que SIEMPRE falla en silencio para un parámetro (UndeclaredVariableError capturada y separada con 'pass'), así que la mutación nunca se guardaba — el chain devolvía el valor correcto una vez, pero la variable quedaba intacta. Retorna (result, mutated: bool) """ # Dividir la cadena en pasos respetando paréntesis steps = self._split_method_chain(chain_str) if not steps: return origin_value, False current_value = origin_value current_type = _get_value_type(current_value) should_mut = False origin_var = var_name chain_position = 0 # 0 = primera posición for step in steps: step = step.strip() # ── .mut — terminador de cadena ──────────────────────────────────── if step == 'mut': if chain_position == 0: raise InvalidOperationError( "'.mut' no puede usarse directamente sobre el origen sin una operación previa.") if is_const: raise InvalidOperationError( f"No se puede mutar la constante '{origin_var}' con '.mut'.") should_mut = True continue # .mut no cambia current_value, solo activa la mutación # ── Parsear nombre del método y argumentos ───────────────────────── m = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)\s*(?:\((.*)\))?$', step, re.DOTALL) if not m: raise InvalidOperationError(f"Método inválido en cadena: '{step}'") method_name = m.group(1) raw_args_str = m.group(2) # None si es atributo sin paréntesis # Parsear argumentos method_args = [] if raw_args_str is not None and raw_args_str.strip(): method_args = self._parse_call_parameters({'value': raw_args_str}) # ── Buscar en la tabla del tipo actual ───────────────────────────── current_type = _get_value_type(current_value) type_methods = _CORE_TYPE_METHODS.get(current_type, {}) if method_name not in type_methods: raise InvalidOperationError( f"El tipo '{current_type}' no tiene el método o atributo '{method_name}'.") method_def = type_methods[method_name] cat = method_def['cat'] # ── Validar posición ─────────────────────────────────────────────── if cat == 'chain_only' and chain_position == 0: raise InvalidOperationError( f"'{method_name}' solo puede usarse encadenado, no directamente sobre el origen.") # ── Validar que first no se encadena después de otra operación ───── # 'first' sí puede ir en posición 0 Y también producir resultado para # encadenar si el resultado tiene tipo con métodos. No se bloquea. # ── Ejecutar el método ───────────────────────────────────────────── fn = method_def.get('fn') if fn is None: # Método especial sin fn directa (como mut ya manejado arriba) chain_position += 1 continue try: current_value = fn(current_value, method_args) except Exception as e: raise InvalidOperationError( f"Error ejecutando '{method_name}' sobre '{current_type}': {e}") # ── Mutables por defecto (push, pop, etc.) ───────────────────────── if method_def.get('mutable_default') and not is_const: # AGREGADO: si la variable es una colección tipada (p.ej. # int arr = [...]), validar y coaccionar (int↔float↔bool) el # nuevo estado para que respete el tipo declarado en todos sus # elementos antes de mutar. current_value = self._enforce_collection_type_for_var(origin_var, current_value, method_name) # Mutar inmediatamente sin necesitar .mut # REPARADO: soporta persistir tanto en symbol_table como en # function_param_values (ver docstring de param_frame arriba). if param_frame: self.symbol_table.declare_function_param_value(param_frame, origin_var, current_value) else: try: self.symbol_table.set_value(origin_var, current_value) except UndeclaredVariableError: pass # si es parámetro o temporal, no mutar chain_position += 1 # ── Aplicar .mut si fue solicitado ───────────────────────────────────── if should_mut: # AGREGADO: misma validación/coerción de colección tipada antes de mutar con .mut current_value = self._enforce_collection_type_for_var(origin_var, current_value) # REPARADO: soporta persistir tanto en symbol_table como en # function_param_values (ver docstring de param_frame arriba). Antes # esto SIEMPRE usaba symbol_table.set_value, que revienta con # UndeclaredVariableError apenas 'origin_var' es un parámetro de # función — por eso '.mut' parecía "no servir" cuando se usaba # dentro de una función sobre uno de sus parámetros. if param_frame: self.symbol_table.declare_function_param_value(param_frame, origin_var, current_value) self._log(f"[TypeMethod] .mut aplicado (parámetro): '{origin_var}' = {current_value}") else: try: self.symbol_table.set_value(origin_var, current_value) self._log(f"[TypeMethod] .mut aplicado: '{origin_var}' = {current_value}") except UndeclaredVariableError: raise InvalidOperationError( f"No se puede mutar '{origin_var}': variable no encontrada.") return current_value, should_mut def _split_method_chain(self, chain_str: str) -> list: """ Divide 'toUpperCase().trim().slice(0,3).mut' en ['toUpperCase()', 'trim()', 'slice(0,3)', 'mut'] respetando paréntesis anidados. """ steps = [] current = [] depth = 0 i = 0 s = chain_str.strip() while i < len(s): c = s[i] if c == '(': depth += 1 current.append(c) elif c == ')': depth -= 1 current.append(c) elif c == '.' and depth == 0: part = ''.join(current).strip() if part: steps.append(part) current = [] else: current.append(c) i += 1 part = ''.join(current).strip() if part: steps.append(part) return steps def handle_PerformWhileLoop(self, node): condition_str = node["value"] self._log(f"Iniciando bucle 'perform-while' (do-while). La condición a chequear es: {condition_str}") self.break_flag = False while True: self._log(" Ejecutando bloque del perform-while (al menos una vez).") # CORRECCIÓN: Manejar tanto listas (formato actual del AST) como diccionarios block_content = node["block"] if isinstance(block_content, list): self.symbol_table.push_scope(label="perform-while") for statement in block_content: if self.break_flag or self.return_flag: break self.execute_node(statement) if self.return_flag: self.symbol_table.pop_scope(label="perform-while") return self.symbol_table.pop_scope(label="perform-while") elif isinstance(block_content, dict): self.execute_block(block_content) # Verificación de Break if self.break_flag: self._log(" 'Break' detectado. Saliendo del bucle.") break if self.return_flag: self._log(" 'Return' detectado. Saliendo del bucle.") break # Evaluar la condición para decidir si repetir if not self.evaluate_expression(condition_str): self._log(f" La condición '{condition_str}' ahora es falsa. Saliendo del bucle.") break self.break_flag = False self._log("Finalizado bucle 'perform-while'.") def handle_SwitchStatement(self, node): switch_var_name = node["value"] switch_value = None if hasattr(self, '_current_function') and self._current_function: try: switch_value = self.symbol_table.get_function_param_value( self._current_function, switch_var_name ) except UndeclaredVariableError: pass if switch_value is None: switch_value = self.symbol_table.get_value(switch_var_name) self._log(f"Iniciando 'switch' para la variable '{switch_var_name}' (valor: {switch_value})") self.break_flag = False self.switch_fall_through = False # NUEVA LÓGICA: Los casos están directamente en el nodo SwitchStatement cases = node.get("cases", []) default_case = node.get("defaultCase") a_case_matched = False # Procesar todos los casos for case_node in cases: case_value = case_node.get("case") # Si encontramos coincidencia o estamos en fall-through if not self.switch_fall_through and str(case_value) == str(switch_value): self._log(f" Coincidencia encontrada en 'case {case_value}'. Ejecutando bloque.") self.switch_fall_through = True a_case_matched = True # Ejecutar el bloque del caso si estamos en fall-through if self.switch_fall_through: # Ejecutar todas las instrucciones del bloque del caso block_content = case_node.get("block", []) self.symbol_table.push_scope(label=f"switch case {case_value}") for statement in block_content: if self.break_flag or self.return_flag: break self.execute_node(statement) if self.return_flag: self.symbol_table.pop_scope(label=f"switch case {case_value}") return self.symbol_table.pop_scope(label=f"switch case {case_value}") if self.break_flag: self._log(" 'Break' detectado en 'switch'. Saliendo.") self.break_flag = False self.switch_fall_through = False self._log("Finalizado 'switch'.") return # Procesar caso default si no hubo coincidencia o estamos en fall-through if default_case and (self.switch_fall_through or not a_case_matched): self._log(f" Ejecutando bloque 'default'.") block_content = default_case.get("block", []) self.symbol_table.push_scope(label="switch default") for statement in block_content: if self.break_flag or self.return_flag: break self.execute_node(statement) if self.return_flag: self.symbol_table.pop_scope(label="switch default") return self.symbol_table.pop_scope(label="switch default") self.break_flag = False self.switch_fall_through = False self._log("Finalizado 'switch'.") # ============================================================================== # AGREGADO: dump de estado de debug compartido (éxito Y error) # ============================================================================== # Antes había dos versiones: la del camino "éxito" (completa: símbolos + # structs + objetos OOP + instancias de struct + funciones/tareas async) y la # del camino "error" (sólo `print(interpreter.symbol_table)`, sin el resto, y # sin comprobar `debug_mode` — por lo que aparecía SIEMPRE, hubiera o no -d). # Se unifica en una sola función para que el error muestre exactamente el # mismo estado actualizado que el éxito, y sólo cuando debug_mode está activo. def _print_debug_state(interpreter): if not interpreter.debug_mode: return print("\n==============================================") print("--- Estado final de la Tabla de Símbolos ---") print("==============================================") print(interpreter.symbol_table) print("\n==============================================") print("--- Tabla de Structs ---") print("==============================================") print(interpreter.struct_table) # Mostrar instancias de struct en el scope global _struct_instances_found = False for scope_i, scope in enumerate(interpreter.symbol_table.symbols): for vname, sym in scope.items(): if isinstance(sym.value, StructInstance): if not _struct_instances_found: print("\n Instancias de Struct (scope global):") _struct_instances_found = True print(f" [{vname}] → {sym.value.format_print(vname)}") print("\n==============================================") print("--- Tabla de Objetos (OOP) ---") print("==============================================") print(interpreter.object_table) if interpreter._async_functions: print("\n--- Funciones Async declaradas ---") print(" ", sorted(interpreter._async_functions)) if interpreter._async_tasks: print(f"\n--- Tareas async pendientes: {len(interpreter._async_tasks)} ---") def main(): global debug_mode_g # Parsear argumentos: -d activa debug, --source es el .tss real json_file_path = None source_tss_file = None source_fallback_file = None _argv = sys.argv[1:] _i = 0 while _i < len(_argv): _arg = _argv[_i] if _arg in ('-d', '--d', '--debug'): debug_mode_g = True elif _arg == '--source' and _i + 1 < len(_argv): _i += 1 source_tss_file = _argv[_i] elif _arg == '--source-fallback' and _i + 1 < len(_argv): _i += 1 source_fallback_file = _argv[_i] elif json_file_path is None and not _arg.startswith('--'): json_file_path = _arg _i += 1 if json_file_path is None: print("Uso: python interpre.py [-d] ") sys.exit(1) # ── Cargar AST según la extensión del archivo ───────────────────────── if json_file_path.endswith('.tsslk') or json_file_path.endswith('.tbc'): # Bytecode Tesseract → decodificar con tsslk_decoder try: from tsslk_decoder import decode_tbc ast_data = decode_tbc(json_file_path) except FileNotFoundError: print(f"Error: No se encontró el archivo '{json_file_path}'.") return except Exception as e: print(f"Error al decodificar bytecode: {e}") import traceback; traceback.print_exc() return else: # JSON AST normal try: with open(json_file_path, "r", encoding="utf-8") as f: ast_data = json.load(f) except FileNotFoundError: print(f"Error: No se encontró el archivo '{json_file_path}'.") return except json.JSONDecodeError: print(f"Error: El archivo JSON en '{json_file_path}' está mal formado.") return interpreter = Interpreter(debug_mode=debug_mode_g) if _tsc_engine is not None and source_tss_file: try: tsc_cfg = _tsc_engine.load_and_validate( source_tss_file, debug=debug_mode_g) interpreter._tsc_config = tsc_cfg except (ValueError, FileNotFoundError): pass # Si falla la carga del .tsc, modo libre sin error fatal # Si se recibió el .tss real via --source, setearlo ANTES de interpret() if source_tss_file and os.path.isfile(source_tss_file): interpreter._source_file = os.path.abspath(source_tss_file) # .tss reconstruido desde el AST (fallback de CONTENIDO si el real falla # al abrirse). No afecta el nombre que aparece en el traceback. if source_fallback_file and os.path.isfile(source_fallback_file): interpreter._source_fallback_file = os.path.abspath(source_fallback_file) import time inicio = time.perf_counter() # Usar perf_counter para mayor precisión try: interpreter.interpret(ast_data, source_path=json_file_path) # Calcular tiempo transcurrido fin = time.perf_counter() tiempo_transcurrido = fin - inicio if interpreter.debug_mode: # Mostrar tiempo de ejecución con diferentes niveles de precisión print(f"\n⏱️ Tiempo de ejecución total:") if tiempo_transcurrido < 0.001: # Menos de 1 milisegundo microsegundos = tiempo_transcurrido * 1_000_000 print(f" {microsegundos:.2f} microsegundos") elif tiempo_transcurrido < 1: # Menos de 1 segundo milisegundos = tiempo_transcurrido * 1_000 print(f" {milisegundos:.2f} milisegundos") else: # Para tiempos mayores a 1 segundo, mostrar formato completo horas = int(tiempo_transcurrido // 3600) minutos = int((tiempo_transcurrido % 3600) // 60) segundos = tiempo_transcurrido % 60 if horas > 0: print(f" {horas}h {minutos}m {segundos:.3f}s") elif minutos > 0: print(f" {minutos}m {segundos:.3f}s") else: print(f" {segundos:.6f} segundos") # Mostrar siempre el total en segundos con alta precisión print(f" (Total: {tiempo_transcurrido:.9f} segundos)") if interpreter.debug_mode: _print_debug_state(interpreter) except TesseractError as e: fin = time.perf_counter() tiempo_transcurrido = fin - inicio call_stack = getattr(interpreter, '_call_stack', []) source_lines = getattr(interpreter, '_source_lines', None) if call_stack or source_lines: print(format_traceback(e, call_stack, source_lines)) else: print(f'\n\u274c ERROR DURANTE LA EJECUCIÓN:\n{e}') print(f'\n\u23f1\ufe0f Tiempo transcurrido hasta el error: {tiempo_transcurrido:.6f} segundos') # AGREGADO: mismo dump completo y actualizado que el camino de éxito, # y sólo si debug_mode está activo (antes salía siempre, con o sin -d). _print_debug_state(interpreter) if __name__ == "__main__": main()