From b31ba536e4a1b980299fd01456ac79e3e3554ffd Mon Sep 17 00:00:00 2001 From: KeshavAnandCode Date: Sat, 16 May 2026 12:06:21 -0500 Subject: [PATCH] added decompilation and decompile with portffix in json --- packages/foreignthon/pyproject.toml | 2 +- packages/foreignthon/src/foreignthon/cli.py | 40 ++++++++ .../foreignthon/src/foreignthon/transpiler.py | 93 ++++++++++++++++--- packages/langs/es/pyproject.toml | 4 +- packages/langs/es/src/foreignthon_es/es.json | 5 +- packages/langs/ta/pyproject.toml | 4 +- packages/langs/ta/src/foreignthon_ta/ta.json | 16 +++- 7 files changed, 140 insertions(+), 24 deletions(-) diff --git a/packages/foreignthon/pyproject.toml b/packages/foreignthon/pyproject.toml index f361eb6..512f22e 100644 --- a/packages/foreignthon/pyproject.toml +++ b/packages/foreignthon/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "foreignthon" -version = "0.3.0" +version = "0.4.0" description = "Write Python in any language. Transpiles foreign-language .xx.py files to standard Python." license = { text = "GPL v3" } requires-python = ">=3.9" diff --git a/packages/foreignthon/src/foreignthon/cli.py b/packages/foreignthon/src/foreignthon/cli.py index 505d1a3..72c2d61 100644 --- a/packages/foreignthon/src/foreignthon/cli.py +++ b/packages/foreignthon/src/foreignthon/cli.py @@ -115,8 +115,48 @@ def validate_pack(json_file: Path): click.echo(f"✓ Pack '{data['meta']['name']}' is valid.") + +@main.command(context_settings=CONTEXT_SETTINGS) +@click.argument("file", type=click.Path(exists=True, path_type=Path)) +@click.option("--lang", "-l", required=True, help="Target language code (e.g. es, ta)") +@click.option("--postfix", is_flag=True, help="Use @@ postfix style for if/elif/while") +@click.option("--output", "-o", default=None, help="Output file or directory") +def decompile(file: Path, lang: str, postfix: bool, output: str | None): + """ + Convert standard Python back to a foreign language. + + Keywords and builtins are translated. Variable names are untouched. + + \b + fpy decompile script.py --lang es + fpy decompile script.py --lang ta --postfix + fpy decompile script.py --lang es -o out/ + """ + from .transpiler import detranspile_file + + result = detranspile_file(file, lang, postfix=postfix) + + ext = f".{lang}.py" + + if output is None: + stem = file.stem if not file.stem.endswith(f".{lang}") else file.stem + out_path = file.with_name(stem + ext) + else: + out = Path(output) + if out.is_dir() or str(output).endswith("/"): + out.mkdir(parents=True, exist_ok=True) + out_path = out / (file.stem + ext) + else: + out_path = out + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(result, encoding="utf-8") + click.echo(f"Decompiled: {out_path}") + + def _lang_from_file(path: Path) -> str: suffixes = path.suffixes if len(suffixes) >= 2 and suffixes[-1] == ".py": return suffixes[-2].lstrip(".") return "en" + diff --git a/packages/foreignthon/src/foreignthon/transpiler.py b/packages/foreignthon/src/foreignthon/transpiler.py index bcf66f2..19c44f4 100644 --- a/packages/foreignthon/src/foreignthon/transpiler.py +++ b/packages/foreignthon/src/foreignthon/transpiler.py @@ -9,11 +9,6 @@ from .pack import load_pack def _apply_postfix_syntax(source: str, mapping: dict) -> str: - """ - Pre-tokenizer pass: handle postfix @@ keyword syntax. - x > 0 @@ஆனால்: → ஆனால் x > 0: - Indentation is preserved by separating it before rewriting. - """ if "@@" not in source: return source @@ -28,28 +23,50 @@ def _apply_postfix_syntax(source: str, mapping: dict) -> str: result.append(line) continue - # Separate indentation from content so we never lose it stripped = line.lstrip() indent = line[: len(line) - len(stripped)] ending = "\n" if stripped.endswith("\n") else "" content = stripped.rstrip("\n") def _replace(m: re.Match) -> str: - expr = m.group(1).strip() - kw = m.group(2) - return f"{kw} {expr}" + return f"{m.group(2)} {m.group(1).strip()}" - rewritten = indent + postfix_re.sub(_replace, content) + ending - result.append(rewritten) + result.append(indent + postfix_re.sub(_replace, content) + ending) + + return "".join(result) + + +def _apply_postfix_output(source: str, en_to_foreign: dict, postfix_english: set) -> str: + """ + Post-pass for decompile: rewrite foreign keyword lines to @@ postfix. + postfix_english comes from the language pack's postfix_keywords list. + """ + postfix_foreign = {en_to_foreign[k] for k in postfix_english if k in en_to_foreign} + + lines = source.splitlines(keepends=True) + result = [] + + for line in lines: + stripped = line.lstrip() + indent = line[: len(line) - len(stripped)] + ending = "\n" if line.endswith("\n") else "" + content = stripped.rstrip("\n") + matched = False + + for fkw in postfix_foreign: + if content.startswith(fkw + " ") and content.endswith(":"): + expr = content[len(fkw): -1].strip() + result.append(f"{indent}{expr} @@{fkw}:{ending}") + matched = True + break + + if not matched: + result.append(line) return "".join(result) def transpile(source: str, lang_code: str) -> str: - """ - Transpile foreign-language Python source to standard Python. - Uses the tokenizer so strings and comments are never touched. - """ pack = load_pack(lang_code) mapping: dict[str, str] = {} @@ -86,6 +103,47 @@ def transpile(source: str, lang_code: str) -> str: return "".join(result) +def detranspile(source: str, lang_code: str, postfix: bool = False) -> str: + pack = load_pack(lang_code) + + en_to_foreign: dict[str, str] = {} + for section in ("keywords", "builtins", "exceptions", "stdlib"): + for foreign, english in pack[section].items(): + en_to_foreign[english] = foreign + + tokens_in = tokenize.generate_tokens(io.StringIO(source).readline) + result: list[str] = [] + prev_end = (1, 0) + + for tok in tokens_in: + tok_type, tok_string, tok_start, tok_end, _ = tok + + start_row, start_col = tok_start + end_row, end_col = prev_end + + if start_row == end_row: + result.append(" " * (start_col - end_col)) + else: + result.append("\n" * (start_row - end_row)) + result.append(" " * start_col) + + if tok_type == tokenize.NAME and tok_string in en_to_foreign: + result.append(en_to_foreign[tok_string]) + else: + result.append(tok_string) + + prev_end = tok_end + + output = "".join(result) + + if postfix: + # Use pack-defined list, fallback to sensible defaults + postfix_english = set(pack.get("postfix_keywords", ["if", "elif", "while"])) + output = _apply_postfix_output(output, en_to_foreign, postfix_english) + + return output + + def transpile_file(path: Path) -> str: lang_code = _detect_lang(path) source = path.read_text(encoding="utf-8") @@ -93,6 +151,11 @@ def transpile_file(path: Path) -> str: return transpile(source, lang_code) +def detranspile_file(path: Path, lang_code: str, postfix: bool = False) -> str: + source = path.read_text(encoding="utf-8") + return detranspile(source, lang_code, postfix=postfix) + + def run_transpiled(original_path: Path, transpiled: str) -> None: import linecache diff --git a/packages/langs/es/pyproject.toml b/packages/langs/es/pyproject.toml index bad8f1d..9e8a0c4 100644 --- a/packages/langs/es/pyproject.toml +++ b/packages/langs/es/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "foreignthon-es" -version = "0.1.1" +version = "0.2.10" description = "Spanish language pack for ForeignThon." license = { text = "GPL v3" } requires-python = ">=3.9" @@ -14,7 +14,7 @@ authors = [ keywords = ["foreignthon", "spanish", "español"] dependencies = [ - "foreignthon>=0.1.0", + "foreignthon>=0.4.0", ] [project.entry-points."foreignthon.langs"] diff --git a/packages/langs/es/src/foreignthon_es/es.json b/packages/langs/es/src/foreignthon_es/es.json index 2797de1..4c57e30 100644 --- a/packages/langs/es/src/foreignthon_es/es.json +++ b/packages/langs/es/src/foreignthon_es/es.json @@ -141,5 +141,6 @@ "colecciones": "collections", "ruta": "pathlib", "expresion_regular": "re" - } -} + }, + "postfix_keywords": [] +} \ No newline at end of file diff --git a/packages/langs/ta/pyproject.toml b/packages/langs/ta/pyproject.toml index 4b45289..94410d7 100644 --- a/packages/langs/ta/pyproject.toml +++ b/packages/langs/ta/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "foreignthon-ta" -version = "0.1.1" +version = "0.2.0" description = "Tamil language pack for ForeignThon." license = { text = "GPL v3" } requires-python = ">=3.9" @@ -12,7 +12,7 @@ authors = [ { name = "Keshav Anand", email = "keshavanand.dev@gmail.com" } ] keywords = ["foreignthon", "tamil", "தமிழ்"] -dependencies = ["foreignthon>=0.1.0"] +dependencies = ["foreignthon>=0.4.0"] [project.entry-points."foreignthon.langs"] ta = "foreignthon_ta" diff --git a/packages/langs/ta/src/foreignthon_ta/ta.json b/packages/langs/ta/src/foreignthon_ta/ta.json index b8c4b9c..97a57a2 100644 --- a/packages/langs/ta/src/foreignthon_ta/ta.json +++ b/packages/langs/ta/src/foreignthon_ta/ta.json @@ -126,5 +126,17 @@ "தொகுப்புகள்": "collections", "பாதை": "pathlib", "வழக்கமொழி": "re" - } -} + }, + "postfix_keywords": [ + "if", + "elif", + "while", + "def", + "class", + "for", + "with", + "try", + "except", + "finally" + ] +} \ No newline at end of file