from __future__ import annotations import ast import textwrap from pathlib import Path import pytest from foreignthon.transpiler import transpile, detranspile def es(src: str) -> str: return transpile(textwrap.dedent(src).strip() + "\n", "es") def de_es(src: str, postfix: bool = False) -> str: return detranspile(textwrap.dedent(src).strip() + "\n", "es", postfix=postfix) def valid(src: str) -> bool: try: ast.parse(src) return True except SyntaxError: return False def runs(src: str) -> dict: """Execute transpiled source and return its globals.""" code = compile(src, "", "exec") glob = {} exec(code, glob) return glob # --------------------------------------------------------------------------- # Complex class with methods, properties, exceptions # --------------------------------------------------------------------------- def test_class_with_methods(): src = """ clase Contador: def __init__(self, inicio=0): self.valor = inicio def incrementar(self): self.valor += 1 retornar self.valor def reiniciar(self): self.valor = 0 c = Contador(10) c.incrementar() c.incrementar() """ out = es(src) assert valid(out) g = runs(out) assert g["c"].valor == 12 # --------------------------------------------------------------------------- # Exception handling with custom exception # --------------------------------------------------------------------------- def test_exception_handling(): src = """ clase MiError(Excepcion): pasar def dividir(a, b): si b == 0: lanzar ErrorDeDivisionCero("no dividas por cero") retornar a / b intentar: resultado = dividir(10, 2) excepto ErrorDeDivisionCero como e: resultado = -1 finalmente: hecho = Verda """ out = es(src) assert valid(out) g = runs(out) assert g["resultado"] == 5.0 assert g["hecho"] is True # --------------------------------------------------------------------------- # Generator with yield # --------------------------------------------------------------------------- def test_generator(): src = """ def cuadrados(n): para i en dist(n): generar i * i resultado = lista(cuadrados(5)) """ out = es(src) assert valid(out) g = runs(out) assert g["resultado"] == [0, 1, 4, 9, 16] # --------------------------------------------------------------------------- # Lambda and higher order functions # --------------------------------------------------------------------------- def test_lambda_and_builtins(): src = """ nums = [3, 1, 4, 1, 5, 9, 2, 6] pares = lista(filtrar(lambda x: x % 2 == 0, nums)) dobles = lista(map(lambda x: x * 2, nums)) total = sum(nums) mayor = max(nums) menor = min(nums) """ out = es(src) assert valid(out) g = runs(out) assert g["pares"] == [4, 2, 6] assert g["total"] == 31 assert g["mayor"] == 9 assert g["menor"] == 1 # --------------------------------------------------------------------------- # Nested functions and closures # --------------------------------------------------------------------------- def test_nested_functions(): src = """ def hacer_multiplicador(n): def multiplicar(x): retornar x * n retornar multiplicar doble = hacer_multiplicador(2) triple = hacer_multiplicador(3) resultado = doble(5) + triple(4) """ out = es(src) assert valid(out) g = runs(out) assert g["resultado"] == 22 # --------------------------------------------------------------------------- # While loop with break and continue # --------------------------------------------------------------------------- def test_while_break_continue(): src = """ resultado = [] i = 0 mientras i < 20: i += 1 si i % 2 == 0: continuar si i > 9: parar resultado.append(i) """ out = es(src) assert valid(out) g = runs(out) assert g["resultado"] == [1, 3, 5, 7, 9] # --------------------------------------------------------------------------- # List/dict/set comprehensions # --------------------------------------------------------------------------- def test_comprehensions(): src = """ cuadrados = [x*x para x en dist(6)] pares = {x para x en dist(10) si x % 2 == 0} cubo_dict = {x: x**3 para x en dist(5)} """ out = es(src) assert valid(out) g = runs(out) assert g["cuadrados"] == [0, 1, 4, 9, 16, 25] assert g["pares"] == {0, 2, 4, 6, 8} assert g["cubo_dict"] == {0: 0, 1: 1, 2: 8, 3: 27, 4: 64} # --------------------------------------------------------------------------- # @@ postfix syntax — mixed with prefix # --------------------------------------------------------------------------- def test_postfix_mixed_with_prefix(): src = """ def clasificar(n): n > 0 @@si: retornar "positivo" n < 0 @@osi: retornar "negativo" sino: retornar "cero" resultados = [clasificar(x) para x en [-2, 0, 3]] """ out = es(src) assert valid(out) assert "@@" not in out g = runs(out) assert g["resultados"] == ["negativo", "cero", "positivo"] # --------------------------------------------------------------------------- # @@ postfix in while and nested ifs # --------------------------------------------------------------------------- def test_postfix_while_nested(): src = """ acum = 0 i = 1 i <= 10 @@mientras: i % 2 == 0 @@si: acum += i i += 1 """ out = es(src) assert valid(out) assert "@@" not in out g = runs(out) assert g["acum"] == 30 # 2+4+6+8+10 # --------------------------------------------------------------------------- # Strings and comments never touched # --------------------------------------------------------------------------- def test_strings_with_keyword_names(): src = """ msg = "si para mientras def class" comentario = 'si esto no se traduce' fstr = f"valor si={42}" lista_kw = ["si", "para", "mientras"] """ out = es(src) assert '"si para mientras def class"' in out assert "'si esto no se traduce'" in out assert '["si", "para", "mientras"]' in out def test_comment_lines_untouched(): src = """ # si para mientras escribir dist x = 1 # si esto es un comentario y = 2 """ out = es(src) assert "# si para mientras escribir dist" in out assert "# si esto es un comentario" in out # --------------------------------------------------------------------------- # Spacing — no double blank lines, no spaces around parens # --------------------------------------------------------------------------- def test_no_double_blank_lines(): src = """ def foo(): pasar def bar(): pasar def baz(): pasar """ out = es(src) assert "\n\n\n" not in out def test_no_spaces_around_parens(): src = "escribir(dist(10))\n" out = es(src) assert "print(range(10))" in out # --------------------------------------------------------------------------- # Decompile — round trip # --------------------------------------------------------------------------- def test_decompile_postfix(): src = textwrap.dedent(""" def check(x): if x > 0: print(x) elif x == 0: print(0) else: pass """).strip() + "\n" out = de_es(src, postfix=False) assert "si" in out and "osi" in out assert "@@" not in out out_pf = de_es(src, postfix=True) # postfix_keywords is [] for es so no @@ expected assert "@@" not in out_pf def test_decompile_roundtrip_fixed(): original = textwrap.dedent(""" def sumar(a, b): return a + b for i in range(5): print(sumar(i, 1)) """).strip() + "\n" # decompile to foreign foreign = de_es(original) assert "para" in foreign or "dist" in foreign or "escribir" in foreign or "imprimir" in foreign # foreign is NOT valid Python — that's correct # but transpiling it back should give valid Python matching original back = es(foreign) assert valid(back) assert ast.dump(ast.parse(original)) == ast.dump(ast.parse(back)) def test_decompile_exceptions_fixed(): src = textwrap.dedent(""" try: x = 1 / 0 except ZeroDivisionError: x = 0 """).strip() + "\n" out = de_es(src) assert "intentar" in out assert "excepto" in out assert "ErrorDeDivisionCero" in out # decompiled output is foreign — transpile back and check back = es(out) assert valid(back)