Spaces:
Sleeping
Sleeping
| 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'|<struct_name> | |
| 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': <val>, '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<p1.name=null, p1.edad=0, ...> — 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"<StructInstance {self.struct_name}#{self._id}>" | |
| 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 ──────────────────────────────────────────────────── | |
| 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 | |
| def kind(self): | |
| return self._kind | |
| # ── Paso efectivo ───────────────────────────────────────────────────────── | |
| 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<null>' | |
| 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('<Q', struct.pack('<d', f))[0]) for f in items] | |
| return [] | |
| def _binary(v, args): | |
| """ | |
| binary(n_bytes=1, type='int', signed=False) | |
| Convierte cada elemento del rango a binario real de n_bytes bytes. | |
| No muta. | |
| """ | |
| _must_be_range(v) | |
| n_bytes = int(args[0]) if args else 1 | |
| typ = str(args[1]).strip().strip("'\"") if len(args) > 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 | |
| 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} = <null>" | |
| 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', '<método>') | |
| 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 = "<unknown>" # 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 = "<unknown>" | |
| 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"<Error: No se puede acceder a {array_name}[{index_value}]>" | |
| except UndeclaredVariableError: | |
| return f"<Error: Variable no definida>" | |
| 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"<Error: Sintaxis inválida: {array_expression!r}>" | |
| 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"<Error: Estructura corrupta en '{base_expr}'>" | |
| # ── 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"<Error interno en nivel {level}: {e}>" | |
| 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"<Error interno en nivel {level}: {e}>" | |
| 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"<Error: Acceso por rango no válido sobre tipo " | |
| f"'{type(structure).__name__}'>") | |
| 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"<Error: El rango de índices debe ser de enteros, se encontró '{idx}'>" | |
| length = len(structure) | |
| if isinstance(structure, dict): | |
| keys = list(structure.keys()) | |
| result = {} | |
| for idx in expanded: | |
| if idx < 0 or idx >= length: | |
| return (f"<Error: Índice de rango {idx} fuera de límites del dict " | |
| f"(0..{length-1})>") | |
| 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"<Error: Índice de rango {idx} fuera de límites " | |
| f"(0..{length-1})>") | |
| 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"<Error: {e}>" | |
| # 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('<Error'): | |
| return result | |
| if level < len(indices_list) - 1: | |
| current = result | |
| continue | |
| return result | |
| if not isinstance(current, (list, tuple, dict)): | |
| return (f"<Error: '{label}' nivel {level} es de tipo " | |
| f"'{type(current).__name__}' y no es indexable>") | |
| 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"<Error: Índice {index_value} fuera de límites del dict>" | |
| else: | |
| return f"<Error: Clave '{index_value}' no existe en dict>" | |
| else: | |
| if not isinstance(index_value, int): | |
| try: | |
| index_value = int(index_value) | |
| except (TypeError, ValueError): | |
| return f"<Error: Índice debe ser entero para array/tupla>" | |
| length = len(current) | |
| if -length <= index_value < length: | |
| current = current[index_value] | |
| else: | |
| return f"<Error: Índice {index_value} fuera de límites (0..{length-1})>" | |
| except Exception as e: | |
| return f"<Error interno nivel {level}: {e}>" | |
| 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('<Error'): | |
| return full_match | |
| return json.dumps(result) if isinstance(result, str) else str(result) | |
| prev = None | |
| while prev != expression_str: | |
| prev = expression_str | |
| expression_str = re.sub(pattern, replace_access, expression_str) | |
| return expression_str | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # AGREGADO: evaluador maestro — reemplazo de eval() para evaluate_expression | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # Todo lo que hacía evaluate_expression ANTES de llegar al eval() (resolver | |
| # mod.func(), llamadas embebidas, acceso indexado, parámetros, structs/OOP/ | |
| # métodos core, variables normales, &&/||) NO CAMBIA — sigue exactamente | |
| # igual. Lo único que cambia es CÓMO se evalúa el texto final resultante: | |
| # en vez de eval(), se parte a mano (sin eval, sin exec, sin nodos tipo | |
| # BinOp/UnaryOp — sólo texto + regex + recursión, en el mismo estilo que | |
| # el resto del intérprete) respetando la MISMA precedencia que Python: | |
| # | |
| # or > 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'//|(?<!\*)\*(?!\*)|(?<!/)/(?!/)|%') | |
| def _me_mask(self, expr): | |
| """ | |
| Devuelve una copia de `expr` (mismo largo/índices) donde: | |
| - el contenido de strings ("..."/'...') se reemplaza por 'x', | |
| - el contenido INTERIOR de paréntesis/corchetes/llaves (a | |
| cualquier profundidad) se reemplaza por 'x', | |
| - los literales numéricos completos (incluida notación | |
| científica: 1e-05) se reemplazan por 'N', para que un '-'/'+' | |
| de exponente no se confunda con un operador. | |
| Así se puede buscar con regex los operadores de NIVEL SUPERIOR | |
| (or, and, ==, +, *, etc.) sin caer dentro de un literal. | |
| """ | |
| out = list(expr) | |
| depth = 0 | |
| in_str = False | |
| str_ch = '"' | |
| i = 0 | |
| n = len(expr) | |
| while i < n: | |
| ch = expr[i] | |
| if in_str: | |
| if ch == str_ch and (i == 0 or expr[i - 1] != '\\'): | |
| in_str = False | |
| else: | |
| out[i] = 'x' | |
| i += 1 | |
| continue | |
| if ch in ('"', "'"): | |
| in_str = True | |
| str_ch = ch | |
| i += 1 | |
| continue | |
| if ch in '([{': | |
| depth += 1 | |
| i += 1 | |
| continue | |
| if ch in ')]}': | |
| depth -= 1 | |
| i += 1 | |
| continue | |
| if depth > 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"<literal:{_num_txt}>", _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( | |
| "<literal>", _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 != "<unknown>" 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 != "<unknown>" 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 != "<unknown>" 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 != "<unknown>" 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"<Error: Variable '{raw_argument}' no definida>" | |
| 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(<expr>) | |
| print(<expr>, 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<p1.name=null, ...> ────────── | |
| 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<p1.dir.calle=null, ...> ── | |
| 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(<var>) | |
| read(<var>, <tipo>) | |
| read(<var>, <tipo>, <msg>) | |
| read(<var>, <msg>) <- msg detectado como string literal | |
| read(void) <- AGREGADO: getchar() de C (lee 1 char y lo descarta) | |
| read(void, <msg>) <- getchar() con prompt (msg se muestra sin salto de línea) | |
| read(<var>, void) <- tipo void = getchar(), igual que arriba | |
| read(<var>, 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 __<módulo>_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) | |
| # ========================================================================== | |
| 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') | |
| 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) | |
| 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 <file> → archivo intermedio (.txt) | |
| 2. astAjson.py <intermediate>→ 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 != "<unknown>" 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 <file> 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] <ruta_al_archivo_json|.tsslk|.tbc>") | |
| 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() |