Fran23135's picture
Upload 409 files
3637f8d verified
Raw
History Blame Contribute Delete
486 kB
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 ────────────────────────────────────────────────────
@staticmethod
def _infer_kind(start, end):
if start is None and end is None:
return RangeValue._KIND_NULL
if isinstance(start, float) or isinstance(end, float):
return RangeValue._KIND_FLOAT
if isinstance(start, int) and isinstance(end, int):
return RangeValue._KIND_INT
if isinstance(start, str) and isinstance(end, str):
return RangeValue._KIND_STRING
return RangeValue._KIND_INT
@property
def kind(self):
return self._kind
# ── Paso efectivo ─────────────────────────────────────────────────────────
@property
def effective_step(self):
if self._step is not None:
return self._step
return 0.1 if self._kind == self._KIND_FLOAT else 1
# ── Expansión bajo demanda ────────────────────────────────────────────────
def expand(self) -> list:
"""Genera la lista de valores del rango."""
if self._kind == self._KIND_NULL:
return []
if self._kind == self._KIND_INT:
end = self.end if not self.half_open else self.end - 1
return list(range(self.start, end + 1,
max(1, int(self.effective_step))))
if self._kind == self._KIND_FLOAT:
import decimal as _dec
step = _dec.Decimal(str(self.effective_step))
cur = _dec.Decimal(str(self.start))
stop = _dec.Decimal(str(self.end))
if self.half_open:
# excluir end
result = []
while cur < stop:
result.append(float(cur))
cur += step
return result
result = []
while cur <= stop:
result.append(float(cur))
cur += step
if result and _dec.Decimal(str(result[-1])) < stop:
result.append(float(stop))
return result
if self._kind == self._KIND_STRING:
s_cp = ord(self.start)
e_cp = ord(self.end) if not self.half_open else ord(self.end) - 1
step = max(1, int(self.effective_step))
return [chr(cp) for cp in range(s_cp, e_cp + 1, step)]
return []
# ── Longitud ──────────────────────────────────────────────────────────────
def length(self) -> int:
return len(self.expand())
# ── Representación ────────────────────────────────────────────────────────
def format_display(self) -> str:
"""Range<0,1,2,3>"""
if self._kind == self._KIND_NULL:
return 'Range<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
@staticmethod
def _normalize_value(value):
if isinstance(value, float):
return float(f'{value:.15g}'.rstrip('0').rstrip('.') or '0')
return value
def push_scope(self, label=""):
"""Crea un nuevo scope local."""
self.symbols.append({})
self._context_stack.append(label)
def pop_scope(self, label=""):
if len(self.symbols) > 1:
scope = self.symbols[-1]
if scope:
ctx = self._context_stack[:-1] # contexto padre
path = " > ".join(c for c in ctx if c)
lbl = f"{path} > {label}" if path else label
self.scope_history[lbl] = {k: v.value for k, v in scope.items()}
if self._context_stack:
self._context_stack.pop()
self.symbols.pop()
def _debug_log(self, message):
"""Log interno de la tabla, activado desde el intérprete."""
if getattr(self, '_debug_mode', False):
print(message)
def declare(self, name, symbol):
"""Declara un nuevo símbolo."""
self.symbols[-1][name] = symbol
def set_value(self, name, value):
for scope in reversed(self.symbols):
if name in scope:
sym = scope[name]
if sym.is_const:
# Caso: const con valor null → permite UNA asignación
if sym.value is None and not sym._null_assigned:
# REPARADO: la única asignación permitida debe
# respetar el tipo declarado cuando la constante es
# TIPADA (ej. 'const int x = null;'). null NUNCA es
# un valor válido para esa asignación en ese caso —
# null sólo se permitía en la declaración. Para una
# constante DINÁMICA (declared_type == 'dynamic'),
# cualquier valor —incluido null— es válido.
if not _value_matches_declared_type(value, sym.declared_type):
_got = 'null' if value is None else _get_value_type(value)
raise TypeExplicitConflictError(
f"No se puede asignar un valor de tipo '{_got}' a la "
f"constante '{name}': fue declarada con tipo "
f"'{sym.declared_type}', y esta es su única asignación "
f"permitida (ya tenía valor null)."
)
# REPARADO: cualquier asignación —incluido null—
# cuenta como la única permitida. Antes, si el valor
# asignado era null, NO se marcaba _null_assigned,
# permitiendo reasignar indefinidamente mientras se
# siguiera asignando null; ahora null también fija
# el valor final y bloquea asignaciones futuras.
sym._null_assigned = True
sym.value = value
return
# Ya tiene valor no-null o ya fue asignada → error
raise ConstError(
msg("var.const_reassign", name=name)
)
sym.value = value
return
raise NameError(
msg("name.undefined", name=name)
)
def get_value(self, name):
"""Obtiene el valor actual de un símbolo."""
for scope in reversed(self.symbols):
if name in scope:
return scope[name].value
if str(name).lower() == 'true': return True
if str(name).lower() == 'false': return False
try: return int(name)
except ValueError:
try: return float(name)
except ValueError: raise UndeclaredVariableError(name)
def push_function_frame(self, func_name):
"""Crea un nuevo frame para una llamada recursiva."""
if func_name not in self.function_param_values:
self.function_param_values[func_name] = []
self.function_param_values[func_name].append({})
def pop_function_frame(self, func_name):
"""Elimina el frame actual al retornar de la función."""
if func_name in self.function_param_values and self.function_param_values[func_name]:
self.function_param_values[func_name].pop()
def get_symbol(self, name):
"""Obtiene el objeto Symbol completo."""
for scope in reversed(self.symbols):
if name in scope:
return scope[name]
raise UndeclaredVariableError(name)
def declare_function(self, name, function_node):
"""Declara una nueva función."""
self.function_symbols[name] = function_node
def declare_function_params(self, func_name, params):
"""Declara los parámetros de una función."""
self.function_params[func_name] = params
def get_function(self, name):
"""Obtiene la definición de una función."""
if name not in self.function_symbols:
raise UndeclaredVariableError(f"Función '{name}' no ha sido declarada.")
return self.function_symbols[name]
def get_function_params(self, func_name):
"""Obtiene los parámetros de una función."""
return self.function_params.get(func_name, {})
def declare_function_param_value(self, func_name, param_name, value):
"""Guarda el valor de un parámetro para una función específica"""
if func_name not in self.function_param_values:
self.function_param_values[func_name] = [{}]
self.function_param_values[func_name][-1][param_name] = value
def get_function_param_value(self, func_name, param_name):
"""Obtiene el valor de un parámetro de una función"""
if (func_name in self.function_param_values and
self.function_param_values[func_name] and
param_name in self.function_param_values[func_name][-1]):
return self.function_param_values[func_name][-1][param_name]
raise UndeclaredVariableError(f"Parámetro '{param_name}' no tiene valor en función '{func_name}'")
def __str__(self):
# Variables normales
def _fmt(v):
if isinstance(v, float):
s = f'{v:.15f}'.rstrip('0')
return s if not s.endswith('.') else s + '0'
return v
scopes_list = []
for i, scope in enumerate(self.symbols):
label = "Global" if i == 0 else f"Local {i}"
scopes_list.append(f"{label}: { {k: _fmt(v.value) for k, v in scope.items()} }")
symbols_str = f"Variables:\n " + "\n ".join(scopes_list)
# Funciones declaradas
functions_str = f"Funciones: {list(self.function_symbols.keys())}"
# Parámetros CON SUS VALORES CORRECTOS
params_with_values = {}
for func_name, params_info in self.function_params.items():
func_params = {}
for param_name, param_type in params_info.items():
# Buscar en la tabla específica de valores de parámetros
if (func_name in self.function_param_values and
param_name in self.function_param_values[func_name]):
param_value = self.function_param_values[func_name][param_name]
func_params[param_name] = f"{param_type} = {param_value}"
else:
func_params[param_name] = f"{param_type} = <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)
# ==========================================================================
@staticmethod
def _is_source_file_import(module_str: str) -> bool:
"""
Detecta si el nodo LibraryCall apunta a un archivo fuente (.tss).
El parser puede dejar las comillas o quitarlas; ambos casos se manejan.
"""
clean = module_str.strip('"\'')
return clean.lower().endswith('.tss')
@staticmethod
def _normalize_file_path(raw: str, base_dir: str) -> str:
"""
Normaliza una ruta de archivo para Linux, Windows y macOS.
• Elimina comillas envolventes
• Convierte separadores de Windows (\\ o /) al separador del SO actual
• Resuelve rutas relativas contra base_dir
• Aplica normpath para eliminar '..', '.' redundantes
"""
path = raw.strip('"\'')
# Unificar separadores: \\ y / → os.sep
path = path.replace('\\\\', os.sep).replace('\\', os.sep).replace('/', os.sep)
# Rutas relativas → resolver contra el directorio base
if not os.path.isabs(path):
path = os.path.join(base_dir, path)
return os.path.normpath(path)
@staticmethod
def _derive_module_name(file_path: str) -> str:
"""
Deriva el nombre de módulo a partir de la ruta del archivo:
'/path/to/mycode.tss' → 'mycode'
"""
print(os.path.splitext(os.path.basename(file_path))[0])
return os.path.splitext(os.path.basename(file_path))[0]
def _run_compiler_pipeline(self, file_path: str) -> dict:
"""
Ejecuta el pipeline completo sobre un archivo .tss:
1. compiler.exe <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()