From 3a32fd8c4b575337a35d40e08695072a1478be29 Mon Sep 17 00:00:00 2001 From: KeshavAnandCode Date: Fri, 15 May 2026 18:46:57 -0500 Subject: [PATCH] transpiler, errors --- .../foreignthon/src/foreignthon/errors.py | 55 ++++++++++++ .../foreignthon/src/foreignthon/transpiler.py | 83 +++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/packages/foreignthon/src/foreignthon/errors.py b/packages/foreignthon/src/foreignthon/errors.py index e69de29..7f88d5a 100644 --- a/packages/foreignthon/src/foreignthon/errors.py +++ b/packages/foreignthon/src/foreignthon/errors.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import sys +import traceback + +from .pack import load_pack, PackNotFoundError + +_active_lang: str | None = None + + +def activate(lang_code: str) -> None: + """Install the bilingual error hook for the given language.""" + global _active_lang + _active_lang = lang_code + sys.excepthook = _bilingual_hook + + +def deactivate() -> None: + """Restore the default Python error hook.""" + global _active_lang + _active_lang = None + sys.excepthook = sys.__excepthook__ + + +def _bilingual_hook(exc_type, exc_value, exc_tb) -> None: + """ + Print errors in foreign language first, English below. + + [ES] ErrorDeValor: el argumento debe ser un número entero + [EN] ValueError: argument must be an integer + Traceback ... + """ + try: + pack = load_pack(_active_lang) + error_messages = pack.get("error_messages", {}) + exceptions = pack.get("exceptions", {}) + + # Reverse exceptions map: English name → foreign name + en_to_foreign = {v: k for k, v in exceptions.items()} + + en_name = exc_type.__name__ + foreign_name = en_to_foreign.get(en_name, en_name) + foreign_msg = error_messages.get(en_name, str(exc_value)) + lang_code = pack["meta"]["code"].upper() + + print(f"[{lang_code}] {foreign_name}: {foreign_msg}", file=sys.stderr) + print(f"[EN] {en_name}: {exc_value}", file=sys.stderr) + print(file=sys.stderr) + + except (PackNotFoundError, Exception): + # If anything goes wrong with translation, fall back silently + pass + + # Always print the standard traceback at the end + traceback.print_exception(exc_type, exc_value, exc_tb) diff --git a/packages/foreignthon/src/foreignthon/transpiler.py b/packages/foreignthon/src/foreignthon/transpiler.py index e69de29..9409448 100644 --- a/packages/foreignthon/src/foreignthon/transpiler.py +++ b/packages/foreignthon/src/foreignthon/transpiler.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import io +import tokenize +from pathlib import Path + +from .pack import load_pack + + +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) + + # Build a single flat lookup: foreign token -> English token + mapping: dict[str, str] = {} + mapping.update(pack["keywords"]) + mapping.update(pack["builtins"]) + mapping.update(pack["exceptions"]) + mapping.update(pack["stdlib"]) + + 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 + + # Preserve original whitespace/indentation between tokens + 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) + + # Only swap NAME tokens — leaves strings, comments, ops untouched + if tok_type == tokenize.NAME and tok_string in mapping: + result.append(mapping[tok_string]) + else: + result.append(tok_string) + + prev_end = tok_end + + return "".join(result) + + +def transpile_file(path: Path) -> str: + """ + Detect language from file extension (.es.py → es), + read the file, and return transpiled Python source. + """ + lang_code = _detect_lang(path) + source = path.read_text(encoding="utf-8") + + # Allow shebang-style override: # foreignthon: fr + lang_code = _check_shebang(source, lang_code) + + return transpile(source, lang_code) + + +def _detect_lang(path: Path) -> str: + """Extract lang code from extension, e.g. script.es.py → es.""" + suffixes = path.suffixes # e.g. ['.es', '.py'] + if len(suffixes) >= 2 and suffixes[-1] == ".py": + return suffixes[-2].lstrip(".") + raise ValueError( + f"Cannot detect language from filename '{path.name}'. " + "Expected format: script..py (e.g. script.es.py)" + ) + + +def _check_shebang(source: str, default: str) -> str: + """Check first line for # foreignthon: override.""" + first_line = source.splitlines()[0] if source else "" + if first_line.startswith("# foreignthon:"): + return first_line.split(":", 1)[1].strip() + return default