TajikLang — майдончаи озмоишӣ

Пайвастшавӣ… (бори аввал каме вақт мегирад)
'''\n", "checker.py": "\"\"\"Санҷанда — checks a program before a single line of it runs.\n\nThis is the thing Python cannot do. Python finds a misspelled name only when\nthat line executes, so a typo inside a branch that runs once a month is a bug\nthat ships. TajikLang builds a complete AST before executing anything, so it\ncan walk that tree first and report **every** problem it is sure about, all at\nonce, before any output appears.\n\nWhat it reports, and never guesses about:\n\n* a name used but never declared\n* assignment to a name that was never declared\n* a call to a known function with the wrong number of arguments\n* code after `баргардон`, `шикан` or `давом` that can never run\n* `бигзор` on a name already taken in the same function\n\nand, as warnings that do not stop the program:\n\n* a variable declared and never read\n\nThe rule this file lives by: **no false alarms**. A checker that cries wolf is\nworse than no checker, because students learn to ignore it. Anything it cannot\nbe certain of, it says nothing about.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\n\nfrom . import ast_nodes as ast\n\n\n@dataclass\nclass Diagnostic:\n \"\"\"One problem found before running.\"\"\"\n\n message: str\n line: int\n column: int\n hint: str = \"\"\n is_warning: bool = False\n\n @property\n def kind(self) -> str:\n return \"Огоҳӣ (Warning)\" if self.is_warning else \"Хатои санҷиш (CheckError)\"\n\n def format(self, source_lines: list[str]) -> str:\n \"\"\"Rendered exactly like a runtime error, so students see one style.\"\"\"\n parts = [f\"{self.kind} (сатри {self.line}, сутуни {self.column}):\", \"\"]\n if 1 <= self.line <= len(source_lines):\n parts.append(\" \" + source_lines[self.line - 1].rstrip())\n parts.append(\" \" + \" \" * max(self.column - 1, 0) + \"^\")\n parts.append(\"\")\n parts.append(self.message)\n if self.hint:\n parts.append(f\"Маслиҳат: {self.hint}\")\n return chr(10).join(parts)\n\n\nclass _Scope:\n def __init__(\n self,\n parent: \"_Scope | None\",\n is_function: bool = False,\n is_builtin: bool = False,\n ) -> None:\n self.names: dict[str, ast.Node] = {}\n self.read: set[str] = set()\n self.parent = parent\n self.is_function = is_function\n self.is_builtin = is_builtin\n\n def declare(self, name: str, node: ast.Node) -> None:\n self.names[name] = node\n\n def has(self, name: str) -> bool:\n if name in self.names:\n return True\n return self.parent is not None and self.parent.has(name)\n\n def mark_read(self, name: str) -> None:\n scope: _Scope | None = self\n while scope is not None:\n if name in scope.names:\n scope.read.add(name)\n return\n scope = scope.parent\n\n def taken_in_function(self, name: str) -> bool:\n \"\"\"Mirrors Environment.has_within_function exactly.\n\n Including the part that matters most: builtins do not count, because\n the runtime lets a program shadow them. A checker that disagrees with\n the interpreter is worse than no checker.\n \"\"\"\n if self.is_builtin:\n return False\n if name in self.names:\n return True\n if self.is_function or self.parent is None:\n return False\n return self.parent.taken_in_function(name)\n\n\nclass Checker:\n def __init__(self, program: ast.Program, builtin_names: set[str]) -> None:\n self.program = program\n self.builtins = builtin_names\n self.diagnostics: list[Diagnostic] = []\n\n # name -> parameter count, for user functions we can see\n self.arities: dict[str, int] = {}\n\n # ------------------------------------------------------------------\n def check(self) -> list[Diagnostic]:\n # Builtins sit in their own scope above the globals, mirroring the\n # interpreter, so that `бигзор дарозӣ = 7.5` is allowed here too.\n builtins = _Scope(None, is_builtin=True)\n for name in self.builtins:\n builtins.declare(name, self.program)\n builtins.read.add(name)\n\n globals_ = _Scope(builtins, is_function=True)\n self._block(self.program.statements, globals_, report_unused=False)\n self.diagnostics.sort(key=lambda d: (d.line, d.column))\n return self.diagnostics\n\n def _error(self, node: ast.Node, message: str, hint: str = \"\") -> None:\n self.diagnostics.append(Diagnostic(message, node.line, node.column, hint))\n\n def _warn(self, node: ast.Node, message: str, hint: str = \"\") -> None:\n self.diagnostics.append(\n Diagnostic(message, node.line, node.column, hint, is_warning=True)\n )\n\n # ------------------------------------------------------------------\n def _block(\n self,\n statements: list[ast.Statement],\n scope: _Scope,\n report_unused: bool = True,\n ) -> None:\n # Functions first. At runtime a function body does not run until it is\n # called, by which time a function declared further down exists — so\n # mutual recursion is legal and must not be reported.\n for statement in statements:\n if isinstance(statement, ast.FunctionDeclaration):\n scope.declare(statement.name, statement)\n self.arities[statement.name] = len(statement.parameters)\n elif isinstance(statement, ast.ClassDeclaration):\n scope.declare(statement.name, statement)\n scope.read.add(statement.name)\n # A class is called to make an object, and its argument count\n # is `оғоз` minus the `худ` the language supplies.\n for method in statement.methods:\n if method.name == \"оғоз\":\n self.arities[statement.name] = max(\n 0, len(method.parameters) - 1\n )\n\n stopped: ast.Node | None = None\n for statement in statements:\n if stopped is not None:\n self._error(\n statement,\n \"Ин сатр ҳеҷ гоҳ иҷро намешавад.\",\n hint=f'Пеш аз он дар сатри {stopped.line} \"'\n f'{self._jump_word(stopped)}\" ҳаст.',\n )\n stopped = None # one message per block is enough\n self._statement(statement, scope)\n if isinstance(\n statement,\n (ast.ReturnStatement, ast.BreakStatement, ast.ContinueStatement),\n ):\n stopped = statement\n\n if report_unused:\n for name, node in scope.names.items():\n if name not in scope.read:\n self._warn(\n node,\n f'Тағйирёбандаи \"{name}\" сохта шуд, вале ҳеҷ ҷо '\n \"истифода нашуд.\",\n hint=\"Шояд онро нест кардан мумкин аст.\",\n )\n\n @staticmethod\n def _jump_word(node: ast.Node) -> str:\n if isinstance(node, ast.BreakStatement):\n return \"шикан\"\n if isinstance(node, ast.ContinueStatement):\n return \"давом\"\n return \"баргардон\"\n\n # ------------------------------------------------------------------\n def _statement(self, statement: ast.Statement, scope: _Scope) -> None:\n if isinstance(statement, ast.VarDeclaration):\n self._expression(statement.value, scope)\n if scope.taken_in_function(statement.name):\n self._error(\n statement,\n f'Номи \"{statement.name}\" аллакай муайян шудааст.',\n hint='Барои иваз кардани қимат \"бигзор\" -ро нанависед: '\n f\"{statement.name} = ...\",\n )\n scope.declare(statement.name, statement)\n return\n\n if isinstance(statement, ast.Assignment):\n self._expression(statement.value, scope)\n if not scope.has(statement.name):\n self._error(\n statement,\n f'Номи \"{statement.name}\" муайян нашудааст.',\n hint=f\"Аввал онро эълон кунед: бигзор {statement.name} = ...\",\n )\n else:\n scope.mark_read(statement.name)\n return\n\n if isinstance(statement, ast.IndexAssignment):\n self._expression(statement.target, scope)\n self._expression(statement.index, scope)\n self._expression(statement.value, scope)\n return\n\n if isinstance(statement, ast.ExpressionStatement):\n self._expression(statement.expression, scope)\n return\n\n if isinstance(statement, ast.IfStatement):\n self._expression(statement.condition, scope)\n self._block(statement.then_branch, _Scope(scope))\n if statement.else_branch is not None:\n self._block(statement.else_branch, _Scope(scope))\n return\n\n if isinstance(statement, ast.ForRange):\n self._expression(statement.start, scope)\n self._expression(statement.end, scope)\n self._loop_body(statement.variable, statement.body, scope)\n return\n\n if isinstance(statement, ast.ForEach):\n self._expression(statement.iterable, scope)\n self._loop_body(statement.variable, statement.body, scope)\n return\n\n if isinstance(statement, ast.WhileStatement):\n self._expression(statement.condition, scope)\n self._block(statement.body, _Scope(scope))\n return\n\n if isinstance(statement, ast.FunctionDeclaration):\n # already declared by the hoisting pass above\n inner = _Scope(scope, is_function=True)\n for parameter in statement.parameters:\n inner.declare(parameter, statement)\n inner.read.add(parameter) # a parameter is never \"unused\"\n self._block(statement.body, inner)\n return\n\n if isinstance(statement, ast.ReturnStatement):\n if statement.value is not None:\n self._expression(statement.value, scope)\n return\n\n if isinstance(statement, ast.TryStatement):\n self._block(statement.body, _Scope(scope))\n for clause in statement.handlers:\n handler = _Scope(scope)\n if clause.name is not None:\n handler.declare(clause.name, clause)\n handler.read.add(clause.name) # binding it is enough\n self._block(clause.body, handler)\n return\n\n if isinstance(statement, ast.ClassDeclaration):\n scope.declare(statement.name, statement)\n scope.read.add(statement.name)\n for method in statement.methods:\n inner = _Scope(scope, is_function=True)\n for parameter in method.parameters:\n inner.declare(parameter, method)\n inner.read.add(parameter)\n # A method may call its siblings through `худ`, and member\n # access is not a name lookup, so nothing else to declare.\n self._block(method.body, inner)\n return\n\n if isinstance(statement, ast.MemberAssignment):\n self._expression(statement.target, scope)\n self._expression(statement.value, scope)\n return\n\n if isinstance(statement, ast.ImportStatement):\n scope.declare(statement.name, statement)\n scope.read.add(statement.name) # a module is never \"unused\"\n return\n\n # шикан / давом carry nothing to check\n return\n\n def _loop_body(\n self, variable: str, body: list[ast.Statement], scope: _Scope\n ) -> None:\n inner = _Scope(scope)\n inner.declare(variable, body[0] if body else scope.names.get(variable, None) or _dummy())\n inner.read.add(variable) # a loop variable is never \"unused\"\n self._block(body, inner)\n\n # ------------------------------------------------------------------\n def _expression(self, node: ast.Expression, scope: _Scope) -> None:\n if isinstance(node, ast.Identifier):\n if not scope.has(node.name):\n self._error(\n node,\n f'Номи \"{node.name}\" муайян нашудааст.',\n hint=self._suggest(node.name, scope),\n )\n else:\n scope.mark_read(node.name)\n return\n\n if isinstance(node, ast.UnaryOp):\n self._expression(node.operand, scope)\n return\n\n if isinstance(node, (ast.BinaryOp, ast.LogicalOp)):\n self._expression(node.left, scope)\n self._expression(node.right, scope)\n return\n\n if isinstance(node, ast.ListLiteral):\n for element in node.elements:\n self._expression(element, scope)\n return\n\n if isinstance(node, ast.DictLiteral):\n for key, value in node.pairs:\n self._expression(key, scope)\n self._expression(value, scope)\n return\n\n if isinstance(node, ast.IndexExpression):\n self._expression(node.target, scope)\n self._expression(node.index, scope)\n return\n\n if isinstance(node, ast.MemberExpression):\n self._expression(node.target, scope)\n return\n\n if isinstance(node, ast.CallExpression):\n self._expression(node.callee, scope)\n for argument in node.arguments:\n self._expression(argument, scope)\n\n # Arity is only checked for functions declared in this file and\n # called by their plain name — anything else, stay quiet.\n if isinstance(node.callee, ast.Identifier):\n expected = self.arities.get(node.callee.name)\n if expected is not None and len(node.arguments) != expected:\n self._error(\n node,\n f'Функсияи \"{node.callee.name}\" {expected} аргумент '\n f\"мехоҳад, вале {len(node.arguments)} гирифт.\",\n hint=\"Шумораи аргументҳоро санҷед.\",\n )\n return\n\n # literals carry nothing to check\n return\n\n def _suggest(self, name: str, scope: _Scope) -> str:\n import difflib\n\n visible: list[str] = []\n current: _Scope | None = scope\n while current is not None:\n visible.extend(current.names)\n current = current.parent\n\n matches = difflib.get_close_matches(name, visible, n=1)\n if matches:\n return f'Оё шумо \"{matches[0]}\" -ро дар назар доштед?'\n return f\"Аввал онро эълон кунед: бигзор {name} = ...\"\n\n\ndef _dummy() -> ast.Node:\n return ast.Program(line=1, column=1)\n\n\ndef check_program(\n program: ast.Program, builtin_names: set[str]\n) -> list[Diagnostic]:\n \"\"\"Check a whole program and return every problem, in source order.\"\"\"\n return Checker(program, builtin_names).check()\n", "collation.py": "\"\"\"Тартиби алифбо — ordering text the way Tajik actually orders it.\n\nComparing strings by Unicode code point is wrong for Tajik, and wrong in a way\nthat is invisible until a student sorts a class list.\n\nThe Tajik alphabet places its own letters next to their base letters:\n\n ... в г ғ д ... ғ comes straight after г\n ... и ӣ й к қ ... ӣ after и, қ after к\n\nBut Unicode does not. `г` is U+0433, in the main Cyrillic block, while `ғ` is\nU+0493, off in Cyrillic Supplement. By code point:\n\n \"ғ\" > \"я\" # U+0493 > U+044F\n\nso a naive sort puts every Tajik-specific letter after the entire rest of the\nalphabet. `Ғафуров` would sort after `Яқубов`.\n\nThis module builds a sort key from the real alphabet instead.\n\"\"\"\n\nfrom __future__ import annotations\n\n# The Tajik alphabet in its official order — 35 letters.\nALPHABET = \"абвгғдеёжзиӣйкқлмнопрстуӯфхҳчҷшъэюя\"\n\n# letter -> its position in the alphabet\n_POSITION = {letter: index for index, letter in enumerate(ALPHABET)}\n\n\ndef char_key(char: str) -> tuple[int, int, int]:\n \"\"\"Sort key for a single character.\n\n Returns a tuple compared left to right:\n\n 1. group — 0 for everything that is not a Tajik letter (spaces, digits,\n punctuation, Latin), 1 for Tajik letters. Non-letters sort first.\n 2. position — the alphabet index for Tajik letters, the code point for\n everything else.\n 3. case — 0 for upper case, 1 for lower case, so that `Анор` sorts before\n `анор` but the two stay adjacent instead of being split apart the way\n raw code points split them.\n \"\"\"\n lower = char.lower()\n if lower in _POSITION:\n return (1, _POSITION[lower], 0 if char != lower else 1)\n return (0, ord(char), 0)\n\n\ndef sort_key(text: str) -> list[tuple[int, int, int]]:\n \"\"\"Sort key for a whole string.\"\"\"\n return [char_key(char) for char in text]\n\n\ndef compare(left: str, right: str) -> int:\n \"\"\"-1 if left sorts first, 0 if equal, 1 if right sorts first.\"\"\"\n a, b = sort_key(left), sort_key(right)\n if a < b:\n return -1\n if a > b:\n return 1\n return 0\n", "desktop.py": "\"\"\"Windowed TajikLang Studio launcher.\n\nThis exists separately from the `tajik` command so the desktop application\nnever opens a console window. The installer points its shortcuts here.\n\"\"\"\n\nfrom tajiklang.ide import main\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n", "environment.py": "\"\"\"Лоиҳа ва муҳит — TajikLang's project-local programming environment.\n\nPython needs a separate ``venv`` because a project shares Python's global\npackage world. TajikLang projects carry a deliberately visible `.tajiklang/`\nfolder instead: its configuration and its project-only libraries live beside\nthe student's program and work the same in the standalone application.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nfrom pathlib import Path\n\n\nENV_DIR = \".tajiklang\"\nCONFIG = \"лоиҳа.json\"\nLIBRARY = \"китобхона\"\nMAIN_FILE = \"барнома.tj\"\n\n# A TajikLang project begins with a useful, runnable artefact. Templates are\n# deliberately files rather than hidden generator logic, so students can read\n# every line the moment the project is created.\nPROJECT_KINDS = {\n \"console\": \"Консолӣ\",\n \"web\": \"Сомонаи интерактивӣ\",\n \"game\": \"Бозии хурд\",\n \"blocks\": \"TajikBlocks\",\n \"data\": \"Маълумот ва ҷадвалҳо\",\n}\n\nTEMPLATES = {\n \"console\": (\n \"# Барномаи аввалини ман\\n\"\n 'дода ном <- \"Баҳром\"\\n'\n 'нишон \"Салом\", ном\\n'\n ),\n \"web\": (\n \"# Барои оғоз: tajik веб барнома барнома.tj\\n\"\n \"ворид саҳифа\\n\\n\"\n \"саҳифа.нависед(\\n\"\n ' саҳифа.сарлавҳа(\"Салом аз TajikLang\"),\\n'\n ' саҳифа.матн(\"Ин сомона бо TajikLang сохта шуд.\"),\\n'\n \")\\n\"\n ),\n \"game\": (\n \"# Бозии аввалини ман\\n\"\n \"дода хол <- 0\\n\"\n 'нишон \"Бозӣ оғоз шуд. Хол:\", хол\\n'\n \"# Барои асбобҳои бозӣ: tajik гирифтан бозӣ\\n\"\n ),\n \"blocks\": (\n \"# Ин файлро дар TajikBlocks ё Studio кушоед.\\n\"\n 'дода қаҳрамон <- \"Ситора\"\\n'\n 'нишон \"Салом аз\", қаҳрамон\\n'\n ),\n \"data\": (\n \"# Кор бо маълумот\\n\"\n \"дода холҳо <- [18, 20, 17]\\n\"\n 'нишон \"Шумора:\", дарозӣ(холҳо)\\n'\n \"# Барои ҷадвалҳо: tajik гирифтан ҷадвал\\n\"\n ),\n}\n\n\nclass EnvironmentError(Exception):\n \"\"\"A problem the command line can explain in Tajik.\"\"\"\n\n\ndef find(root: str | Path) -> Path | None:\n \"\"\"Find the nearest TajikLang environment, walking towards the drive root.\"\"\"\n current = Path(root).resolve()\n if current.is_file():\n current = current.parent\n for candidate in (current, *current.parents):\n if (candidate / ENV_DIR / CONFIG).is_file():\n return candidate\n return None\n\n\ndef library(root: str | Path) -> Path | None:\n project = find(root)\n return project / ENV_DIR / LIBRARY if project is not None else None\n\n\ndef create_environment(root: str | Path) -> Path:\n \"\"\"Give an existing folder its own, visible package environment.\"\"\"\n project = Path(root).resolve()\n if not project.is_dir():\n raise EnvironmentError(f'Папкаи \"{project}\" ёфт нашуд.')\n folder = project / ENV_DIR\n config = folder / CONFIG\n if config.exists():\n raise EnvironmentError(\"Ин папка аллакай муҳити TajikLang дорад.\")\n\n (folder / LIBRARY).mkdir(parents=True)\n config.write_text(\n json.dumps(\n {\"навъ\": \"муҳити TajikLang\", \"нусха\": 1, \"китобхона\": LIBRARY},\n ensure_ascii=False,\n indent=2,\n ) + \"\\n\",\n encoding=\"utf-8\",\n )\n return folder\n\n\ndef create_project(\n name: str, parent: str | Path | None = None, kind: str = \"console\"\n) -> Path:\n \"\"\"Create a named project and its visible, project-local environment.\"\"\"\n if not re.fullmatch(r\"[^\\\\/:*?\\\"<>|.][^\\\\/:*?\\\"<>|]*\", name):\n raise EnvironmentError(\"Номи лоиҳа нодуруст аст.\")\n base = Path.cwd() if parent is None else Path(parent)\n project = base / name\n if project.exists():\n raise EnvironmentError(f'Папкаи \"{project}\" аллакай вуҷуд дорад.')\n if kind not in PROJECT_KINDS:\n choices = \", \".join(PROJECT_KINDS)\n raise EnvironmentError(f'Навъи лоиҳаи \"{kind}\" шинохта нашуд. Интихобҳо: {choices}.')\n\n project.mkdir(parents=True)\n create_environment(project)\n (project / MAIN_FILE).write_text(TEMPLATES[kind], encoding=\"utf-8\")\n (project / \"README.md\").write_text(\n f\"# {name}\\n\\n\"\n f\"Ин лоиҳаи TajikLang аст: **{PROJECT_KINDS[kind]}**.\\n\\n\"\n \"- Барномаи асосӣ: `барнома.tj`\\n\"\n \"- Бастаҳои ҳамин лоиҳа: `.tajiklang/китобхона/`\\n\"\n \"- Насби бастаи маҳаллӣ: `tajik гирифтан --лоиҳа омор`\\n\"\n \"- Пешнамоиш дар Studio: тугмаи ▶ Иҷро\\n\",\n encoding=\"utf-8\",\n )\n return project\n", "errors.py": "\"\"\"Хатоҳо — the error system.\n\nEvery error in TajikLang knows four things: what kind of error it is, where it\nhappened, what the source line looked like, and what the student should do\nabout it. A message without a fix is only half an error message.\n\"\"\"\n\nfrom __future__ import annotations\n\n\n# Kinds of runtime failure a program can catch by name:\n#\n# кӯшиш:\n# ...\n# хато \"тақсим_ба_сифр\":\n# ...\n#\n# Only the failures worth handling separately are named. Everything else is\n# ХАТОИ_УМУМӢ, which a bare `хато:` catches along with all the rest.\nKIND_DIVISION = \"тақсим_ба_сифр\"\nKIND_NAME = \"номи_номаълум\"\nKIND_TYPE = \"навъи_нодуруст\"\nKIND_RANGE = \"берун_аз_ҳудуд\"\nKIND_KEY = \"калиди_нест\"\nKIND_ARGUMENTS = \"шумораи_аргумент\"\nKIND_PYTHON = \"хатои_питон\"\nKIND_FILE = \"хатои_файл\"\nKIND_GENERAL = \"умумӣ\"\n\nERROR_KINDS = (\n KIND_DIVISION, KIND_NAME, KIND_TYPE, KIND_RANGE, KIND_KEY,\n KIND_ARGUMENTS, KIND_PYTHON, KIND_FILE, KIND_GENERAL,\n)\n\n\nclass TajikLangError(Exception):\n \"\"\"Base class for every error the language reports to the user.\"\"\"\n\n # Overridden by subclasses; shown as the heading of the message.\n kind: str = \"Хато\"\n\n def __init__(\n self,\n message: str,\n line: int,\n column: int,\n source_line: str = \"\",\n hint: str = \"\",\n call_stack: list[tuple[str, int]] | None = None,\n kind_code: str = KIND_GENERAL,\n ) -> None:\n super().__init__(message)\n self.kind_code = kind_code\n self.message = message\n self.line = line\n self.column = column\n self.source_line = source_line\n self.hint = hint\n\n # Which function called which, innermost last: [(name, call line)].\n # Without this, an error two calls deep names a line and leaves the\n # student with no idea how the program reached it.\n self.call_stack = call_stack or []\n\n def format(self) -> str:\n \"\"\"Render the error the way the student sees it in the terminal.\"\"\"\n parts = [f\"{self.kind} (сатри {self.line}, сутуни {self.column}):\", \"\"]\n\n if self.source_line:\n stripped = self.source_line.rstrip(\"\\n\")\n parts.append(\" \" + stripped)\n # column is 1-based, so column 1 puts the caret under the first char\n parts.append(\" \" + \" \" * max(self.column - 1, 0) + \"^\")\n parts.append(\"\")\n\n parts.append(self.message)\n if self.hint:\n parts.append(f\"Маслиҳат: {self.hint}\")\n\n # Where the failing line was reached from. Without this, an error two\n # calls deep names a line and leaves the student with no idea how the\n # program got there.\n if self.call_stack:\n parts.append(\"\")\n parts.append(\"Роҳи даъват:\")\n for name, line in reversed(self.call_stack):\n parts.append(f' дар функсияи \"{name}\", даъват дар сатри {line}')\n\n return \"\\n\".join(parts)\n\n\n# The English term in brackets is deliberate. A student who meets\n# `Хатои синтаксис (SyntaxError)` here recognises `SyntaxError` the first time\n# they open Python — the Tajik teaches the concept, the bracket carries the\n# vocabulary across. It costs one word per class.\nclass LexerError(TajikLangError):\n \"\"\"A character or sequence the lexer cannot turn into a token.\"\"\"\n\n kind = \"Хатои луғавӣ (LexicalError)\"\n\n\nclass ParseError(TajikLangError):\n \"\"\"Tokens that are individually valid but form no legal construct.\"\"\"\n\n kind = \"Хатои синтаксис (SyntaxError)\"\n\n\nclass RuntimeErrorTJ(TajikLangError):\n \"\"\"A program that parsed fine but failed while running.\"\"\"\n\n kind = \"Хатои иҷро (RuntimeError)\"\n\n\nclass BuiltinError(Exception):\n \"\"\"Raised inside a built-in function, which has no idea where it was called.\n\n The interpreter catches it at the call site and turns it into a proper\n `RuntimeErrorTJ` with the line, column and source line filled in. This is\n the only way a builtin can report a problem without knowing about the AST.\n \"\"\"\n\n def __init__(\n self, message: str, hint: str = \"\", kind_code: str = KIND_GENERAL\n ) -> None:\n super().__init__(message)\n self.message = message\n self.hint = hint\n self.kind_code = kind_code\n\n\ndef internal_report(error: BaseException, where: str = \"\") -> str:\n \"\"\"A fault in the language itself — reported in Tajik, never as a traceback.\n\n A student who meets a crash in the interpreter has done nothing wrong, and\n a wall of English filenames tells them nothing they can act on. So the\n message says, in their language, that this is our bug and how to report\n it — and the technical detail goes to a log file for whoever fixes it.\n\n This is the only place the implementation can surface, and it does not.\n \"\"\"\n import traceback\n\n detail = \"\".join(\n traceback.format_exception(type(error), error, error.__traceback__)\n )\n\n log = \"\"\n try:\n from . import packages\n\n path = packages.home() / \"хатоҳо.log\"\n path.parent.mkdir(parents=True, exist_ok=True)\n with path.open(\"a\", encoding=\"utf-8\") as handle:\n handle.write(f\"--- {where or 'иҷро'} ---{chr(10)}{detail}{chr(10)}\")\n log = str(path)\n except Exception: # noqa: BLE001 - logging must never be the failure\n log = \"\"\n\n lines = [\n \"Хатои дохилии забон.\",\n \"\",\n \"Ин хатои барномаи шумо нест — ин камбудии худи TajikLang аст.\",\n ]\n if log:\n lines.append(f\"Тафсилот дар: {log}\")\n lines.append(\n \"Лутфан хабар диҳед: \"\n \"https://github.com/BahromPython/tajiklang/issues\"\n )\n return chr(10).join(lines)\n", "extras.py": "\"\"\"Useful standard modules that keep their public vocabulary in Tajik.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport urllib.error\nimport urllib.request\nfrom decimal import Decimal\nfrom typing import Any\n\nfrom .errors import BuiltinError\nfrom .values import Builtin, to_tajik_string, type_name\n\n\ndef build_data() -> dict[str, Any]:\n def ба_json(*args: Any) -> str:\n if len(args) != 1:\n raise BuiltinError('Функсияи \"маълумот.ба_json\" 1 аргумент мехоҳад.')\n try:\n return json.dumps(args[0], ensure_ascii=False, default=to_tajik_string)\n except (TypeError, ValueError) as error:\n raise BuiltinError(f\"Қимат ба JSON табдил нашуд: {error}.\") from None\n\n def аз_json(*args: Any) -> Any:\n if len(args) != 1 or not isinstance(args[0], str):\n raise BuiltinError('Функсияи \"маълумот.аз_json\" як матни JSON мехоҳад.')\n try:\n return json.loads(args[0], parse_float=Decimal)\n except json.JSONDecodeError as error:\n raise BuiltinError(\n f\"JSON дуруст нест: сатри {error.lineno}, сутуни {error.colno}.\",\n hint=\"Матн бояд бо {, [, нохунак ва вергулҳои дуруст бошад.\",\n ) from None\n\n return {\"ба_json\": Builtin(\"ба_json\", ба_json), \"аз_json\": Builtin(\"аз_json\", аз_json)}\n\n\ndef build_http() -> dict[str, Any]:\n def хондан(*args: Any) -> str:\n if len(args) != 1 or not isinstance(args[0], str):\n raise BuiltinError('Функсияи \"интернет.хондан\" як суроғаи матнӣ мехоҳад.')\n if not args[0].startswith((\"https://\", \"http://\")):\n raise BuiltinError(\"Суроға бояд бо https:// ё http:// оғоз шавад.\")\n try:\n with urllib.request.urlopen(args[0], timeout=15) as response:\n return response.read().decode(\"utf-8\")\n except (urllib.error.URLError, OSError, UnicodeDecodeError) as error:\n raise BuiltinError(\n f\"Суроға хонда нашуд: {error}.\",\n hint=\"Интернет ва суроғаро санҷед.\",\n ) from None\n\n return {\"хондан\": Builtin(\"хондан\", хондан)}\n", "highlight.py": "\"\"\"Рангкунӣ — deciding what colour each piece of source is.\n\nOne definition, used by the editor and by the website. Two highlighters that\ndisagree about what counts as a keyword is a small bug that shows up in the\ntwo places a newcomer looks first, so there is only one.\n\n`spans(source)` returns non-overlapping `(kind, start, end)` triples in source\norder. Everything not covered is plain text.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\n\nfrom .tokens import KEYWORD_TOKENS\n\n# Longest first, so `то_вақте` is never matched as `то` plus rubbish.\n_KEYWORDS = sorted(KEYWORD_TOKENS, key=len, reverse=True)\n\n# A word boundary that understands Cyrillic. `\\b` and `\\w` are ASCII-minded in\n# some engines, and `тоза` must never light up the `то` inside it.\n_LETTER = r\"[^\\W\\d_]\"\n_WORD = rf\"(? list[tuple[str, int, int]]:\n \"\"\"Where each coloured region starts and ends.\n\n Comments and strings are matched first and claim their whole span, so a\n `#` inside a string, or a keyword inside a comment, is left alone.\n \"\"\"\n found: list[tuple[str, int, int]] = []\n taken: list[tuple[int, int]] = []\n\n def free(start: int) -> bool:\n return not any(a <= start < b for a, b in taken)\n\n for pattern, kind in ((_COMMENT, \"comment\"), (_STRING, \"string\")):\n for match in pattern.finditer(source):\n if free(match.start()):\n found.append((kind, match.start(), match.end()))\n taken.append(match.span())\n\n for match in _KEYWORD.finditer(source):\n if free(match.start()):\n found.append((\"keyword\", match.start(), match.end()))\n\n if builtins:\n for match in _CALL.finditer(source):\n if free(match.start()) and match.group(1) in builtins:\n found.append((\"builtin\", match.start(), match.end()))\n\n for match in _NUMBER.finditer(source):\n if free(match.start()):\n found.append((\"number\", match.start(), match.end()))\n\n found.sort(key=lambda item: item[1])\n return found\n\n\ndef to_html(source: str, builtins: set[str] | None = None) -> str:\n \"\"\"Source as HTML, with each coloured region in a ``.\"\"\"\n def escape(text: str) -> str:\n return (\n text.replace(\"&\", \"&\").replace(\"<\", \"<\").replace(\">\", \">\")\n )\n\n pieces: list[str] = []\n cursor = 0\n for kind, start, end in spans(source, builtins):\n if start < cursor: # keyword sitting inside a longer span\n continue\n pieces.append(escape(source[cursor:start]))\n pieces.append(f'{escape(source[start:end])}')\n cursor = end\n pieces.append(escape(source[cursor:]))\n return \"\".join(pieces)\n", "interop.py": "\"\"\"Пайванд бо Python — the one door out of TajikLang.\n\nTajikLang is a small language. Python has thirty years of libraries. This\nmodule lets an advanced program reach them without the language growing a\nsecond personality:\n\n ворид питон\n\n бигзор омор = питон.ворид(\"statistics\")\n навис(омор.median([3, 1, 4, 1, 5]))\n\nFour rules keep the door from leaking:\n\n1. **One visible prefix.** Everything arrives through `питон.ворид(...)`, so\n an English name in a listing is always traceable to that line. The core\n language stays Tajik.\n2. **Errors come home.** A Python exception is turned into an ordinary Tajik\n runtime error carrying the caller's line, caret and hint. A student must\n never see a bare `ValueError: math domain error` with no position.\n3. **Values convert at the boundary.** Numbers, text, booleans, lists and\n dictionaries pass through as themselves. Anything else stays wrapped and\n prints as `<объекти питон>` rather than pretending to be a TajikLang value.\n4. **It is opt-in.** No program touches Python unless it says `ворид питон`.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport importlib\nfrom decimal import Decimal\nfrom typing import Any\n\nfrom .errors import BuiltinError\nfrom .values import Builtin, Module\n\n\nclass PythonObject:\n \"\"\"A Python value with no TajikLang equivalent, kept at arm's length.\n\n Attribute access and calling both work, so `питон` feels like the rest of\n the language; everything else about it is deliberately opaque.\n \"\"\"\n\n def __init__(self, value: Any, path: str) -> None:\n self.value = value\n self.path = path # e.g. \"statistics.median\", for messages\n\n def __repr__(self) -> str:\n return f\"<объекти питон: {self.path}>\"\n\n\ndef to_tajik(value: Any, path: str) -> Any:\n \"\"\"A Python value on its way into TajikLang.\"\"\"\n if value is None or isinstance(value, (bool, int, str)):\n return value\n if isinstance(value, float):\n # Floats become decimals so that Python's 0.30000000000000004 does not\n # walk in through the back door — see LANGUAGE_SPEC.md §4.1.\n return Decimal(repr(value))\n if isinstance(value, Decimal):\n return value\n if isinstance(value, (list, tuple)):\n return [to_tajik(item, path) for item in value]\n if isinstance(value, dict):\n return {\n key: to_tajik(item, path)\n for key, item in value.items()\n if isinstance(key, (str, int, float, Decimal))\n }\n if callable(value):\n return _wrap_callable(value, path)\n return PythonObject(value, path)\n\n\ndef to_python(value: Any) -> Any:\n \"\"\"A TajikLang value on its way out to Python.\"\"\"\n if isinstance(value, PythonObject):\n return value.value\n if isinstance(value, Decimal):\n # Most libraries want a float; Decimal would raise on arithmetic.\n return float(value)\n if isinstance(value, list):\n return [to_python(item) for item in value]\n if isinstance(value, dict):\n return {key: to_python(item) for key, item in value.items()}\n return value\n\n\ndef _wrap_callable(fn: Any, path: str) -> Builtin:\n def call(*args: Any) -> Any:\n try:\n result = fn(*[to_python(a) for a in args])\n except BuiltinError:\n raise\n except Exception as error: # noqa: BLE001 - any library may raise\n raise BuiltinError(\n f'Функсияи питони \"{path}\" хато дод: '\n f\"{type(error).__name__}: {error}\",\n hint=\"Ин хатои китобхонаи Python аст, на хатои TajikLang.\",\n ) from None\n return to_tajik(result, path)\n\n return Builtin(path, call)\n\n\nclass _PythonNamespace:\n \"\"\"What `питон.ворид(\"math\")` gives back: a module you can reach into.\"\"\"\n\n def __init__(self, module: Any, name: str) -> None:\n self.module = module\n self.name = name\n\n def get(self, attribute: str) -> Any:\n if attribute.startswith(\"_\"):\n raise BuiltinError(\n f'Номи \"{attribute}\" дар питон хусусӣ аст.',\n hint=\"Номҳое, ки бо _ сар мешаванд, дастрас нестанд.\",\n )\n try:\n value = getattr(self.module, attribute)\n except AttributeError:\n raise BuiltinError(\n f'Модули питони \"{self.name}\" номи \"{attribute}\" -ро надорад.',\n hint=\"Номро дар ҳуҷҷатҳои Python санҷед.\",\n ) from None\n return to_tajik(value, f\"{self.name}.{attribute}\")\n\n\nclass PythonModule(Module):\n \"\"\"A Python module dressed as a TajikLang module, resolved lazily.\n\n Lazily because a big library has thousands of attributes and a student\n only ever touches three; walking them all at import time would make\n `питон.ворид(\"pandas\")` take seconds for nothing.\n \"\"\"\n\n def __init__(self, module: Any, name: str) -> None:\n super().__init__(name, None)\n self.namespace = _PythonNamespace(module, name)\n\n def __repr__(self) -> str:\n return f\"<модули питони {self.name}>\"\n\n\ndef build_module() -> dict[str, Any]:\n \"\"\"The `питон` module itself.\"\"\"\n\n def ворид(*args: Any) -> PythonModule:\n if len(args) != 1 or not isinstance(args[0], str):\n raise BuiltinError(\n 'Функсияи \"питон.ворид\" номи модулро ҳамчун матн мехоҳад.',\n hint='Масалан: питон.ворид(\"math\")',\n )\n name = args[0]\n try:\n module = importlib.import_module(name)\n except ImportError:\n raise BuiltinError(\n f'Модули питони \"{name}\" ёфт нашуд.',\n hint=\"Шояд он насб нашудааст.\",\n ) from None\n return PythonModule(module, name)\n\n def даъват(*args: Any) -> Any:\n \"\"\"питон.даъват(объект, \"ном\", аргументҳо...) — call a method.\"\"\"\n if len(args) < 2 or not isinstance(args[1], str):\n raise BuiltinError(\n 'Функсияи \"питон.даъват\" объект ва номи метод мехоҳад.',\n hint='Масалан: питон.даъват(объект, \"sort\")',\n )\n target = to_python(args[0])\n name = args[1]\n try:\n method = getattr(target, name)\n except AttributeError:\n raise BuiltinError(\n f'Объект методи \"{name}\" -ро надорад.'\n ) from None\n return _wrap_callable(method, name)(*args[2:])\n\n return {\n \"ворид\": Builtin(\"ворид\", ворид),\n \"даъват\": Builtin(\"даъват\", даъват),\n }\n", "interpreter.py": "\"\"\"Интерпретатор — walks the AST and makes it happen.\n\nThis is a *tree-walk* interpreter: it visits each node and computes its value\ndirectly, with no bytecode and no compilation step. It is the slowest design\nand by far the clearest one, which is the right trade for a teaching language.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport difflib\nfrom decimal import Decimal\nfrom pathlib import Path\nfrom typing import Any, Callable\n\nfrom . import ast_nodes as ast, collation, stdlib\nfrom .errors import (\n KIND_ARGUMENTS,\n KIND_DIVISION,\n KIND_GENERAL,\n KIND_KEY,\n KIND_NAME,\n KIND_RANGE,\n KIND_TYPE,\n BuiltinError,\n RuntimeErrorTJ,\n)\nfrom .values import (\n BoundMethod,\n BreakSignal,\n Builtin,\n ContinueSignal,\n Instance,\n TajikClass,\n is_whole,\n number,\n Function,\n Module,\n ReturnSignal,\n equals,\n is_number,\n to_tajik_string,\n type_name,\n)\n\n# A loop that runs this many times has almost certainly lost its exit\n# condition. Stopping with an explanation beats a frozen terminal — and beats\n# a frozen browser tab once the playground exists.\nMAX_ITERATIONS = 1_000_000\n\n# How deep function calls may nest before we assume runaway recursion. Kept\n# well under Python's own limit so the student gets a Tajik message rather\n# than a RecursionError traceback.\nMAX_CALL_DEPTH = 200\n\n# How many changes `чаро` remembers per variable.\nHISTORY_LIMIT = 200\n\n\nclass Environment:\n \"\"\"The table of names the program can see.\n\n Each block and each function call gets one of these, chained to the scope\n around it. A name is looked up here first, then in the parent, and so on\n outwards.\n\n `is_function_scope` marks where a function body begins. Declarations look\n for a clashing name only as far as that boundary: a block must not shadow\n the variable beside it, but a function must be free to name its own\n variables without knowing what the caller happened to call theirs.\n \"\"\"\n\n def __init__(\n self,\n parent: \"Environment | None\" = None,\n is_function_scope: bool = False,\n is_builtin_scope: bool = False,\n ) -> None:\n self.values: dict[str, Any] = {}\n self.parent = parent\n self.is_function_scope = is_function_scope\n self.is_builtin_scope = is_builtin_scope\n\n def define(self, name: str, value: Any) -> None:\n \"\"\"Introduce a new name in *this* scope.\"\"\"\n self.values[name] = value\n\n def assign(self, name: str, value: Any) -> bool:\n \"\"\"Change an existing name, wherever it lives. False if unknown.\n\n Assignment stops at the builtin scope: `навис = 5` must not silently\n replace printing. Builtins are readable everywhere and shadowable with\n `бигзор`, but never assignable — a bare `=` means \"change my variable\",\n and a builtin is not the student's variable.\n \"\"\"\n if self.is_builtin_scope:\n return False\n if name in self.values:\n self.values[name] = value\n return True\n if self.parent is not None:\n return self.parent.assign(name, value)\n return False\n\n def get(self, name: str) -> Any:\n if name in self.values:\n return self.values[name]\n if self.parent is not None:\n return self.parent.get(name)\n raise KeyError(name)\n\n def has(self, name: str) -> bool:\n if name in self.values:\n return True\n return self.parent is not None and self.parent.has(name)\n\n def has_within_function(self, name: str) -> bool:\n \"\"\"Is the name already taken by the program in this function's scopes?\n\n Built-in names do not count. The no-shadowing rule exists to stop a\n student clobbering *their own* variable by accident; applying it to\n the standard library would mean every builtin added in a later version\n breaks programs that happened to use that word. `дарозӣ` was a\n perfectly good variable name in 0.2 and became a builtin in 0.7.\n \"\"\"\n if self.is_builtin_scope:\n return False\n if name in self.values:\n return True\n if self.is_function_scope or self.parent is None:\n return False\n return self.parent.has_within_function(name)\n\n def visible_names(self) -> list[str]:\n \"\"\"Every name reachable from here, for \"did you mean\" suggestions.\"\"\"\n names = list(self.values)\n if self.parent is not None:\n names.extend(self.parent.visible_names())\n return names\n\n\nclass Interpreter:\n def __init__(\n self,\n source_lines: list[str] | None = None,\n output: Callable[[str], None] = print,\n base_dir: Path | None = None,\n module_cache: dict[str, Module] | None = None,\n loading: frozenset[str] | None = None,\n read_line: Callable[[str], str | None] | None = None,\n ) -> None:\n self.source_lines = source_lines or []\n self.output = output\n\n # How `хонед` asks for input. The terminal passes Python's `input`,\n # the browser passes a JavaScript prompt, a test passes canned lines.\n # The language itself never learns which.\n self.read_line = read_line\n self.base_dir = base_dir or Path.cwd()\n\n # Shared with the modules this program imports, so a file loaded twice\n # is executed once and `loading` can catch an import cycle.\n self.module_cache = module_cache if module_cache is not None else {}\n self.loading = loading if loading is not None else frozenset()\n\n # Builtins live in their own scope above the globals, so the program\n # can shadow them (see Environment.has_within_function) while still\n # seeing them everywhere it has not.\n self.builtins = Environment(is_builtin_scope=True)\n self.globals = Environment(self.builtins)\n self.environment = self.globals # the scope statements run in\n\n # Which function called which, innermost last. Every error copies it,\n # so a failure two calls deep can show how the program got there.\n self.call_stack: list[tuple[str, int]] = []\n\n # The call site currently being evaluated. A builtin calling back into\n # TajikLang has no node of its own, so errors borrow this one.\n self._current_node: ast.Node = ast.Program(line=1, column=1)\n\n # Every change to every variable, for `чаро`. \"Why is my variable\n # wrong?\" is the commonest question a beginner has, and every other\n # language answers it with silence. A tree-walk interpreter can\n # answer it for almost nothing, because every change already passes\n # through two methods.\n self.history: dict[str, list[tuple[int, Any, str]]] = {}\n\n # Names that were declared inside a block that has since closed. Kept\n # only so that the \"name not defined\" error can explain *why*, which\n # is the single most confusing consequence of block scoping.\n self.closed_names: set[str] = set()\n\n for name, builtin in stdlib.build_globals(\n self.output, self.read_line, self._call_value\n ).items():\n self.builtins.define(name, builtin)\n\n # `чаро` is a real name in the builtin scope, so the checker sees it\n # and a program can shadow it. The interpreter intercepts the call\n # only when this exact object is what the name resolves to — see\n # `_call`. Reaching the builtin body means the argument was not a\n # plain variable name.\n def _чаро_misused(*args: Any) -> None:\n raise BuiltinError(\n 'Функсияи \"чаро\" номи тағйирёбандаро мехоҳад.',\n hint=\"Масалан: чаро(ҷамъ) — бе нохунак ва бе ҳисоб.\",\n )\n\n self._char_builtin = Builtin(\"чаро\", _чаро_misused)\n self.builtins.define(\"чаро\", self._char_builtin)\n\n # ------------------------------------------------------------------\n def _record(self, name: str, line: int, value: Any, note: str) -> None:\n \"\"\"Remember one change, for `чаро`.\n\n Capped per name: a loop running a million times must not turn the\n history into the program's largest object.\n \"\"\"\n entries = self.history.setdefault(name, [])\n if len(entries) < HISTORY_LIMIT:\n entries.append((line, value, note))\n elif len(entries) == HISTORY_LIMIT:\n entries.append((line, value, \"…\"))\n\n def _source_line(self, line: int) -> str:\n if 1 <= line <= len(self.source_lines):\n return self.source_lines[line - 1]\n return \"\"\n\n def _error(\n self,\n node: ast.Node,\n message: str,\n hint: str = \"\",\n kind_code: str = KIND_GENERAL,\n ) -> RuntimeErrorTJ:\n return RuntimeErrorTJ(\n message,\n node.line,\n node.column,\n self._source_line(node.line),\n hint,\n call_stack=list(self.call_stack),\n kind_code=kind_code,\n )\n\n # ------------------------------------------------------------------\n # statements\n # ------------------------------------------------------------------\n def run(self, program: ast.Program, echo: bool = False) -> None:\n \"\"\"Run a whole program. `echo` prints expression values, for the REPL.\"\"\"\n for statement in program.statements:\n if echo and isinstance(statement, ast.ExpressionStatement):\n value = self.evaluate(statement.expression)\n if value is not None:\n self.output(to_tajik_string(value))\n continue\n self.execute(statement)\n\n def execute(self, statement: ast.Statement) -> None:\n if isinstance(statement, ast.VarDeclaration):\n # Checking the enclosing scopes, not just this one, means a block\n # cannot shadow the variable beside it. The search stops at the\n # function boundary — see Environment.has_within_function.\n if self.environment.has_within_function(statement.name):\n raise self._error(\n statement,\n f'Номи \"{statement.name}\" аллакай муайян шудааст.',\n hint='Барои иваз кардани қимат \"бигзор\" -ро нанависед: '\n f\"{statement.name} = ...\",\n )\n value = self.evaluate(statement.value)\n self.environment.define(statement.name, value)\n self._record(statement.name, statement.line, value, \"эълон\")\n return\n\n if isinstance(statement, ast.Assignment):\n value = self.evaluate(statement.value)\n self._record(statement.name, statement.line, value, \"ивазкунӣ\")\n if not self.environment.assign(statement.name, value):\n raise self._error(\n statement,\n f'Номи \"{statement.name}\" муайян нашудааст.',\n hint=f\"Аввал онро эълон кунед: бигзор {statement.name} = ...\",\n )\n return\n\n if isinstance(statement, ast.IndexAssignment):\n self._index_assign(statement)\n return\n\n if isinstance(statement, ast.IfStatement):\n condition = self._as_boolean(\n self.evaluate(statement.condition), statement.condition, 'Шарти \"агар\"'\n )\n if condition:\n self._execute_block(statement.then_branch)\n elif statement.else_branch is not None:\n self._execute_block(statement.else_branch)\n return\n\n if isinstance(statement, ast.ForRange):\n self._for_range(statement)\n return\n\n if isinstance(statement, ast.ForEach):\n self._for_each(statement)\n return\n\n if isinstance(statement, ast.WhileStatement):\n self._while(statement)\n return\n\n if isinstance(statement, ast.FunctionDeclaration):\n if self.environment.has_within_function(statement.name):\n raise self._error(\n statement,\n f'Номи \"{statement.name}\" аллакай муайян шудааст.',\n hint=\"Барои функсия номи дигар интихоб кунед.\",\n )\n self.environment.define(\n statement.name, Function(statement, self.environment)\n )\n return\n\n if isinstance(statement, ast.ReturnStatement):\n value = None\n if statement.value is not None:\n value = self.evaluate(statement.value)\n raise ReturnSignal(value)\n\n if isinstance(statement, ast.ClassDeclaration):\n self._declare_class(statement)\n return\n\n if isinstance(statement, ast.MemberAssignment):\n target = self.evaluate(statement.target)\n if not isinstance(target, Instance):\n raise self._error(\n statement,\n f\"Ба {type_name(target)} майдон илова кардан мумкин нест.\",\n hint=\"Танҳо объектҳои синф майдон доранд.\",\n kind_code=KIND_TYPE,\n )\n target.fields[statement.name] = self.evaluate(statement.value)\n return\n\n if isinstance(statement, ast.BreakStatement):\n raise BreakSignal()\n\n if isinstance(statement, ast.ContinueStatement):\n raise ContinueSignal()\n\n if isinstance(statement, ast.TryStatement):\n self._try(statement)\n return\n\n if isinstance(statement, ast.ImportStatement):\n self._import(statement)\n return\n\n if isinstance(statement, ast.ExpressionStatement):\n self.evaluate(statement.expression)\n return\n\n raise self._error(statement, f\"Амали нофаҳмо: {type(statement).__name__}.\")\n\n def _execute_block(\n self, statements: list[ast.Statement], scope: Environment | None = None\n ) -> None:\n \"\"\"Run a block in a fresh scope chained to the current one.\n\n The scope is restored in a `finally` so that an error — or a\n `баргардон` unwinding out of a function — cannot leave the interpreter\n pointing at a dead environment.\n \"\"\"\n previous = self.environment\n block = scope if scope is not None else Environment(previous)\n self.environment = block\n try:\n for statement in statements:\n self.execute(statement)\n finally:\n self.closed_names.update(block.values)\n self.environment = previous\n\n def _declare_class(self, statement: ast.ClassDeclaration) -> None:\n if self.environment.has_within_function(statement.name):\n raise self._error(\n statement,\n f'Номи \"{statement.name}\" аллакай муайян шудааст.',\n hint=\"Барои синф номи дигар интихоб кунед.\",\n )\n\n parent = None\n if statement.parent is not None:\n if not self.environment.has(statement.parent):\n raise self._error(\n statement,\n f'Синфи волиди \"{statement.parent}\" муайян нашудааст.',\n hint=\"Синфи волидро пеш аз фарзанд эълон кунед.\",\n kind_code=KIND_NAME,\n )\n parent = self.environment.get(statement.parent)\n if not isinstance(parent, TajikClass):\n raise self._error(\n statement,\n f'\"{statement.parent}\" синф нест.',\n hint=\"Пас аз «мерос» бояд номи синф бошад.\",\n kind_code=KIND_TYPE,\n )\n\n methods = {\n method.name: Function(method, self.environment)\n for method in statement.methods\n }\n self.environment.define(\n statement.name, TajikClass(statement.name, parent, methods)\n )\n\n def _instantiate(\n self, cls: TajikClass, arguments: list[Any], node: ast.Node\n ) -> Instance:\n \"\"\"Making an object: create it, then run `оғоз` if the class has one.\"\"\"\n instance = Instance(cls)\n constructor = cls.find(\"оғоз\")\n\n if constructor is None:\n if arguments:\n raise self._error(\n node,\n f'Синфи \"{cls.name}\" функсияи \"оғоз\" надорад, пас '\n \"аргумент қабул намекунад.\",\n hint=\"Функсияи «оғоз(худ, ...)» -ро илова кунед.\",\n kind_code=KIND_ARGUMENTS,\n )\n return instance\n\n self._call_function(\n constructor, [instance] + arguments, node, method_of=cls.name\n )\n return instance\n\n def _try(self, statement: ast.TryStatement) -> None:\n \"\"\"кӯшиш / хато — run a block, and run the handler if it fails.\n\n Only `RuntimeErrorTJ` is caught. `баргардон`, `шикан` and `давом`\n travel as their own signals and pass straight through, so a\n `баргардон` inside `кӯшиш` still returns from the function.\n \"\"\"\n try:\n self._execute_block(statement.body)\n except RuntimeErrorTJ as error:\n for clause in statement.handlers:\n if clause.kind is not None and clause.kind != error.kind_code:\n continue\n scope = Environment(self.environment)\n if clause.name is not None:\n scope.define(clause.name, error.message)\n self._execute_block(clause.body, scope)\n return\n raise # no clause wanted this kind — let it travel on\n\n # ------------------------------------------------------------------\n # loops\n # ------------------------------------------------------------------\n def _loop_scope(self, variable: str, value: Any) -> Environment:\n \"\"\"A fresh scope per iteration, holding just the loop variable.\n\n Per-iteration rather than per-loop, so `бигзор` inside the body works\n on every pass instead of colliding with the previous one.\n \"\"\"\n scope = Environment(self.environment)\n scope.define(variable, value)\n return scope\n\n def _for_range(self, statement: ast.ForRange) -> None:\n start = self.evaluate(statement.start)\n end = self.evaluate(statement.end)\n\n for label, value in ((\"аз\", start), (\"то\", end)):\n if not is_number(value):\n raise self._error(\n statement,\n f'Қимати \"{label}\" бояд рақам бошад, вале {type_name(value)} аст.',\n hint=\"Масалан: барои i аз 1 то 10:\",\n )\n\n # `то` is inclusive: `аз 1 то 5` runs five times, with i = 1..5.\n # A backwards range counts down, so `аз 5 то 1` also runs five times.\n step = 1 if end >= start else -1\n current = start\n count = 0\n\n while (step > 0 and current <= end) or (step < 0 and current >= end):\n count += 1\n self._guard_iterations(count, statement)\n try:\n self._execute_block(\n statement.body, self._loop_scope(statement.variable, current)\n )\n except BreakSignal:\n return\n except ContinueSignal:\n pass\n current += step\n\n def _for_each(self, statement: ast.ForEach) -> None:\n iterable = self.evaluate(statement.iterable)\n\n if isinstance(iterable, list):\n items: list[Any] = list(iterable)\n elif isinstance(iterable, dict):\n items = list(iterable) # walking a dictionary walks its keys\n elif isinstance(iterable, str):\n items = list(iterable)\n else:\n raise self._error(\n statement,\n f\"Аз {type_name(iterable)} гузаштан мумкин нест.\",\n hint=\"Барои шумурдан нависед: барои i аз 1 то 10:\",\n )\n\n for count, item in enumerate(items, start=1):\n self._guard_iterations(count, statement)\n try:\n self._execute_block(\n statement.body, self._loop_scope(statement.variable, item)\n )\n except BreakSignal:\n return\n except ContinueSignal:\n continue\n\n def _while(self, statement: ast.WhileStatement) -> None:\n count = 0\n while self._as_boolean(\n self.evaluate(statement.condition),\n statement.condition,\n 'Шарти \"то_вақте\"',\n ):\n count += 1\n self._guard_iterations(count, statement)\n try:\n self._execute_block(statement.body)\n except BreakSignal:\n return\n except ContinueSignal:\n continue\n\n def _guard_iterations(self, count: int, statement: ast.Node) -> None:\n if count > MAX_ITERATIONS:\n raise self._error(\n statement,\n f\"Давр аз {MAX_ITERATIONS} маротиба зиёд такрор шуд.\",\n hint=\"Шояд шарти хотима ҳеҷ гоҳ иҷро намешавад.\",\n )\n\n # ------------------------------------------------------------------\n # imports\n # ------------------------------------------------------------------\n def _import(self, statement: ast.ImportStatement) -> None:\n name = statement.name\n\n if self.environment.has_within_function(name):\n raise self._error(\n statement,\n f'Номи \"{name}\" аллакай муайян шудааст.',\n hint=\"Модул ва тағйирёбанда наметавонанд як ном дошта бошанд.\",\n )\n\n if name in self.module_cache:\n self.environment.define(name, self.module_cache[name])\n return\n\n if name in stdlib.BUILTIN_MODULES:\n environment = Environment()\n for key, value in stdlib.BUILTIN_MODULES[name](self).items():\n environment.define(key, value)\n module = Module(name, environment)\n else:\n module = self._import_file(statement)\n\n self.module_cache[name] = module\n self.environment.define(name, module)\n\n def _import_file(self, statement: ast.ImportStatement) -> Module:\n from .lexer import Lexer # imported here to avoid a cycle\n from .parser import Parser\n\n name = statement.name\n path = self.base_dir / (name + \".tj\")\n\n # A file beside the program wins; after that, an installed package.\n # That order means a student can always override a library by putting\n # their own file next to the program, which is easier to explain than\n # any search-path rule.\n if not path.exists():\n from . import packages\n\n found = packages.resolve(name, self.base_dir)\n if found is not None:\n path = found\n\n if not path.exists():\n available = \", \".join(sorted(stdlib.BUILTIN_MODULES))\n raise self._error(\n statement,\n f'Модули \"{name}\" ёфт нашуд.',\n hint=f\"На модули дарунсохт ({available}), на файли \"\n f'\"{name}.tj\" дар ин ҷузвдон, на бастаи насбшуда. '\n f\"Барои насб: tajik гирифтан {name}\",\n )\n\n key = str(path.resolve())\n if key in self.loading:\n raise self._error(\n statement,\n f'Модули \"{name}\" худашро ворид мекунад.',\n hint=\"Ду файл наметавонанд якдигарро ворид кунанд.\",\n )\n\n source = path.read_text(encoding=\"utf-8\")\n lexer = Lexer(source, str(path))\n tokens = lexer.tokenize()\n program = Parser(tokens, lexer.lines).parse()\n\n inner = Interpreter(\n lexer.lines,\n self.output,\n base_dir=path.parent,\n module_cache=self.module_cache,\n loading=self.loading | {key},\n read_line=self.read_line,\n )\n inner.run(program)\n return Module(name, inner.globals)\n\n # ------------------------------------------------------------------\n # expressions\n # ------------------------------------------------------------------\n def evaluate(self, node: ast.Expression) -> Any:\n if isinstance(node, (ast.StringLiteral, ast.NumberLiteral)):\n return node.value\n\n if isinstance(node, ast.BooleanLiteral):\n return node.value\n\n if isinstance(node, ast.NullLiteral):\n return None\n\n if isinstance(node, ast.ListLiteral):\n return [self.evaluate(element) for element in node.elements]\n\n if isinstance(node, ast.DictLiteral):\n result: dict[Any, Any] = {}\n for key_node, value_node in node.pairs:\n key = self.evaluate(key_node)\n if not isinstance(key, str) and not is_number(key):\n raise self._error(\n key_node,\n \"Калиди луғат бояд матн ё рақам бошад, вале \"\n f\"{type_name(key)} аст.\",\n hint='Масалан: {\"ном\": \"Баҳром\"}',\n )\n result[key] = self.evaluate(value_node)\n return result\n\n if isinstance(node, ast.Identifier):\n if not self.environment.has(node.name):\n if node.name in self.closed_names:\n raise self._error(\n node,\n f'Номи \"{node.name}\" дар дохили блок эълон шуда буд ва '\n \"берун аз он дида намешавад.\",\n hint=f\"Онро пеш аз блок эълон кунед: бигзор {node.name} = ...\",\n )\n raise self._error(\n node,\n f'Номи \"{node.name}\" муайян нашудааст.',\n hint=self._suggest(node.name),\n kind_code=KIND_NAME,\n )\n return self.environment.get(node.name)\n\n if isinstance(node, ast.UnaryOp):\n if node.operator == \"не\":\n return not self._as_boolean(\n self.evaluate(node.operand), node, 'Амали \"не\"'\n )\n\n operand = self.evaluate(node.operand)\n if not is_number(operand):\n raise self._error(\n node,\n f'Амали \"-\" барои {type_name(operand)} иҷро намешавад.',\n hint=\"Аломати манфӣ танҳо пеши рақам меистад.\",\n )\n return number(-operand)\n\n if isinstance(node, ast.LogicalOp):\n return self._logical(node)\n\n if isinstance(node, ast.BinaryOp):\n return self._binary(node)\n\n if isinstance(node, ast.IndexExpression):\n return self._index(node)\n\n if isinstance(node, ast.MemberExpression):\n return self._member(node)\n\n if isinstance(node, ast.CallExpression):\n return self._call(node)\n\n raise self._error(node, f\"Ифодаи нофаҳмо: {type(node).__name__}.\")\n\n def call_from_host(self, callee: Any, arguments: list[Any]) -> Any:\n \"\"\"Call a TajikLang function from outside the interpreter.\n\n A browser click handler arrives on the JavaScript event loop with no\n statement in progress, so errors are printed rather than raised —\n there is nobody above to catch them.\n \"\"\"\n try:\n return self._call_value(callee, arguments)\n except RuntimeErrorTJ as error:\n self.output(error.format())\n return None\n\n def _explain(self, name: str) -> None:\n \"\"\"`чаро(x)` — print how x came to hold what it holds.\"\"\"\n entries = self.history.get(name)\n if not entries:\n self.output(f'Тағйирёбандаи \"{name}\" ҳеҷ гоҳ иваз нашудааст.')\n return\n\n self.output(f'Тағйирёбандаи \"{name}\":')\n for line, value, note in entries:\n if note == \"…\":\n self.output(\" …\")\n continue\n shown = to_tajik_string(value)\n self.output(f\" сатри {line:<4} → {shown} ({note})\")\n\n def _call_value(self, callee: Any, arguments: list[Any]) -> Any:\n \"\"\"Call a TajikLang value from Python — what `тартиб_бо` needs.\n\n Builtins receive this as a callback, so a builtin can take a function\n as an argument without knowing anything about the AST.\n \"\"\"\n if isinstance(callee, BoundMethod):\n return self._call_function(\n callee.function,\n [callee.instance] + arguments,\n self._current_node,\n method_of=callee.instance.cls.name,\n )\n if isinstance(callee, Function):\n return self._call_function(callee, arguments, self._current_node)\n return callee(*arguments)\n\n # ------------------------------------------------------------------\n # calls\n # ------------------------------------------------------------------\n def _call(self, node: ast.CallExpression) -> Any:\n self._current_node = node\n\n # `чаро(ҷамъ)` is the single place where a call cares about the *name*\n # of its argument rather than its value, so it is handled before the\n # argument is evaluated. It stays an ordinary shadowable name: if the\n # program defines its own `чаро`, that one wins.\n if (\n isinstance(node.callee, ast.Identifier)\n and node.callee.name == \"чаро\"\n and self.environment.has(\"чаро\")\n and self.environment.get(\"чаро\") is self._char_builtin\n and len(node.arguments) == 1\n and isinstance(node.arguments[0], ast.Identifier)\n ):\n self._explain(node.arguments[0].name)\n return None\n\n callee = self.evaluate(node.callee)\n arguments = [self.evaluate(arg) for arg in node.arguments]\n\n if isinstance(callee, TajikClass):\n return self._instantiate(callee, arguments, node)\n\n if isinstance(callee, BoundMethod):\n return self._call_function(\n callee.function,\n [callee.instance] + arguments,\n node,\n method_of=callee.instance.cls.name,\n )\n\n if isinstance(callee, Function):\n return self._call_function(callee, arguments, node)\n\n if isinstance(callee, Builtin) or callable(callee):\n try:\n return callee(*arguments)\n except BuiltinError as error:\n raise self._error(\n node, error.message, error.hint, error.kind_code\n ) from None\n\n raise self._error(\n node,\n f\"{type_name(callee).capitalize()} -ро ҳамчун функсия даъват кардан \"\n \"мумкин нест.\",\n hint=\"Танҳо функсияҳо бо қавсҳо даъват мешаванд.\",\n )\n\n def _call_function(\n self,\n function: Function,\n arguments: list[Any],\n node: ast.Node,\n method_of: str | None = None,\n ) -> Any:\n expected = len(function.parameters)\n if len(arguments) != expected:\n # A method's `худ` is supplied by the language, so the count the\n # student sees must not include it.\n shown = function.parameters[1:] if method_of else function.parameters\n names = \", \".join(shown) or \"ҳеҷ\"\n raise self._error(\n node,\n (\n f'Методи \"{method_of}.{function.name}\" {expected - 1} '\n f\"аргумент мехоҳад, вале {len(arguments) - 1} гирифт.\"\n if method_of\n else f'Функсияи \"{function.name}\" {expected} аргумент '\n f\"мехоҳад, вале {len(arguments)} гирифт.\"\n ),\n hint=f\"Параметрҳо: {names}\",\n kind_code=KIND_ARGUMENTS,\n )\n\n if len(self.call_stack) >= MAX_CALL_DEPTH:\n raise self._error(\n node,\n f'Функсияи \"{function.name}\" аз ҳад зиёд худро даъват кард '\n f\"({MAX_CALL_DEPTH} маротиба).\",\n hint=\"Ҳар функсияи рекурсивӣ бояд ҳолати хотима дошта бошад.\",\n )\n\n # The closure, not the caller's scope: that is what makes the language\n # lexically scoped.\n scope = Environment(function.closure, is_function_scope=True)\n for name, value in zip(function.parameters, arguments):\n scope.define(name, value)\n\n previous = self.environment\n self.environment = scope\n label = f\"{method_of}.{function.name}\" if method_of else function.name\n self.call_stack.append((label, node.line))\n try:\n for statement in function.declaration.body:\n self.execute(statement)\n return None # a function with no баргардон gives холӣ\n except ReturnSignal as signal:\n return signal.value\n finally:\n self.call_stack.pop()\n self.environment = previous\n\n # ------------------------------------------------------------------\n # collections\n # ------------------------------------------------------------------\n def _index(self, node: ast.IndexExpression) -> Any:\n target = self.evaluate(node.target)\n index = self.evaluate(node.index)\n\n if isinstance(target, (list, str)):\n position = self._resolve_index(node, index, len(target))\n if position < 0 or position >= len(target):\n raise self._error(\n node,\n f\"Рақами тартибӣ берун аз ҳудуд аст (дарозӣ {len(target)}).\",\n hint=(\n f\"Рақамҳои имконпазир: 0 то {len(target) - 1}, \"\n f\"ё -1 то -{len(target)} аз охир.\"\n if target\n else \"Ин холӣ аст.\"\n ),\n kind_code=KIND_RANGE,\n )\n return target[position]\n\n if isinstance(target, dict):\n if index not in target:\n raise self._error(\n node,\n f\"Калиди {to_tajik_string(index)} дар луғат нест.\",\n hint=\"Бо функсияи «дорад» санҷед: дорад(луғат, калид)\",\n kind_code=KIND_KEY,\n )\n return target[index]\n\n raise self._error(\n node,\n f\"Аз {type_name(target)} унсур гирифтан мумкин нест.\",\n hint=\"Танҳо рӯйхат, луғат ва матн рақами тартибӣ доранд.\",\n )\n\n def _whole_number(self, node: ast.Node, index: Any) -> int:\n if not is_whole(index):\n raise self._error(\n node,\n f\"Рақами тартибӣ бояд бутун бошад, вале {type_name(index)} аст.\",\n hint=\"Масалан: рӯйхат[0]\",\n )\n return int(index)\n\n def _resolve_index(self, node: ast.Node, index: Any, length: int) -> int:\n \"\"\"Turn a possibly-negative index into a real one.\n\n `рӯйхат[-1]` is the last element, as in Python. Writing\n `рӯйхат[дарозӣ(рӯйхат) - 1]` for something this common is noise.\n \"\"\"\n position = self._whole_number(node, index)\n if position < 0:\n position += length\n return position\n\n def _index_assign(self, statement: ast.IndexAssignment) -> None:\n target = self.evaluate(statement.target)\n index = self.evaluate(statement.index)\n value = self.evaluate(statement.value)\n\n if isinstance(target, list):\n position = self._resolve_index(statement, index, len(target))\n if position < 0 or position >= len(target):\n raise self._error(\n statement,\n f\"Рақами тартибӣ берун аз ҳудуд аст (дарозӣ {len(target)}).\",\n hint=f\"Рақамҳои имконпазир: 0 то {len(target) - 1}, \"\n f\"ё -1 то -{len(target)} аз охир. \"\n \"Барои илова кардан: илова(рӯйхат, қимат)\",\n kind_code=KIND_RANGE,\n )\n target[position] = value\n return\n\n if isinstance(target, dict):\n if not isinstance(index, str) and not is_number(index):\n raise self._error(\n statement,\n \"Калиди луғат бояд матн ё рақам бошад, вале \"\n f\"{type_name(index)} аст.\",\n )\n target[index] = value\n return\n\n if isinstance(target, str):\n raise self._error(\n statement,\n \"Матнро тағйир додан мумкин нест.\",\n hint=\"Матни нав созед: ном = ном + «...»\",\n )\n\n raise self._error(\n statement,\n f\"Ба {type_name(target)} бо рақами тартибӣ қимат додан мумкин нест.\",\n )\n\n def _member(self, node: ast.MemberExpression) -> Any:\n from .interop import PythonModule, PythonObject\n\n target = self.evaluate(node.target)\n\n # A Python module resolves names on demand: a big library has\n # thousands of attributes and a program touches three.\n if isinstance(target, PythonModule):\n try:\n return target.namespace.get(node.name)\n except BuiltinError as error:\n raise self._error(\n node, error.message, error.hint, error.kind_code\n ) from None\n\n if isinstance(target, PythonObject):\n from .interop import to_tajik\n try:\n value = getattr(target.value, node.name)\n except AttributeError:\n raise self._error(\n node,\n f'Объекти питон номи \"{node.name}\" -ро надорад.',\n hint=\"Номро дар ҳуҷҷатҳои Python санҷед.\",\n ) from None\n return to_tajik(value, f\"{target.path}.{node.name}\")\n\n if isinstance(target, Instance):\n if node.name in target.fields:\n return target.fields[node.name]\n method = target.cls.find(node.name)\n if method is not None:\n return BoundMethod(target, method)\n available = \", \".join(\n sorted(set(list(target.fields) + target.cls.method_names()))\n )\n raise self._error(\n node,\n f'Объекти \"{target.cls.name}\" номи \"{node.name}\" -ро надорад.',\n hint=f\"Мавҷуд аст: {available}\" if available else \"\",\n kind_code=KIND_NAME,\n )\n\n if isinstance(target, TajikClass):\n method = target.find(node.name)\n if method is None:\n raise self._error(\n node,\n f'Синфи \"{target.name}\" методи \"{node.name}\" -ро надорад.',\n kind_code=KIND_NAME,\n )\n return method\n\n if isinstance(target, Module):\n if not target.environment.has(node.name):\n available = \", \".join(sorted(target.environment.values))\n raise self._error(\n node,\n f'Модули \"{target.name}\" номи \"{node.name}\" -ро надорад.',\n hint=f\"Мавҷуд аст: {available}\",\n )\n return target.environment.get(node.name)\n\n raise self._error(\n node,\n f'Аз {type_name(target)} бо \".\" чизе гирифтан мумкин нест.',\n hint=\"Аломати «.» танҳо барои модулҳо кор мекунад.\",\n )\n\n # ------------------------------------------------------------------\n # operators\n # ------------------------------------------------------------------\n def _as_boolean(self, value: Any, node: ast.Node, subject: str) -> bool:\n \"\"\"Require a real boolean. There is no truthiness in TajikLang.\n\n `агар ном:` meaning \"if the name is non-empty\" is convenient once and\n confusing thereafter — it is why `if (0)` and `if (\"0\")` disagree\n across languages. Here the student writes what they mean:\n `агар ном != \"\":`.\n \"\"\"\n if isinstance(value, bool):\n return value\n raise self._error(\n node,\n f\"{subject} қимати мантиқӣ мехоҳад, вале {type_name(value)} гирифт.\",\n hint='Муқоиса нависед, масалан: x > 0, ё қиматҳои \"рост\" / \"дурӯғ\".',\n kind_code=KIND_TYPE,\n )\n\n def _logical(self, node: ast.LogicalOp) -> bool:\n \"\"\"`ва` and `ё`, with short-circuiting.\n\n The right side is evaluated only when the left side does not already\n decide the answer. That is why this is not part of `_binary`: for\n `дурӯғ ва (1 / 0)` the division never runs.\n \"\"\"\n subject = f'Амали \"{node.operator}\"'\n left = self._as_boolean(self.evaluate(node.left), node, subject)\n\n if node.operator == \"ва\" and not left:\n return False\n if node.operator == \"ё\" and left:\n return True\n\n return self._as_boolean(self.evaluate(node.right), node, subject)\n\n def _binary(self, node: ast.BinaryOp) -> Any:\n left = self.evaluate(node.left)\n right = self.evaluate(node.right)\n op = node.operator\n\n if op == \"==\":\n return equals(left, right)\n if op == \"!=\":\n return not equals(left, right)\n\n if op in (\"<\", \">\", \"<=\", \">=\"):\n return self._order(node, op, left, right)\n\n # \"+\" is the one operator with more than one meaning.\n if op == \"+\":\n if is_number(left) and is_number(right):\n return number(left + right)\n if isinstance(left, str) and isinstance(right, str):\n return left + right\n if isinstance(left, list) and isinstance(right, list):\n return left + right\n raise self._error(\n node,\n f'Амали \"+\" байни {type_name(left)} ва {type_name(right)} '\n \"иҷро намешавад.\",\n hint=\"Ҳарду бояд рақам бошанд, ё ҳарду матн, ё ҳарду рӯйхат.\",\n )\n\n if not (is_number(left) and is_number(right)):\n raise self._error(\n node,\n f'Амали \"{op}\" байни {type_name(left)} ва {type_name(right)} '\n \"иҷро намешавад.\",\n hint=f'Амали \"{op}\" танҳо барои рақамҳо кор мекунад.',\n )\n\n if op == \"-\":\n return number(left - right)\n if op == \"*\":\n return number(left * right)\n\n if op in (\"/\", \"%\"):\n if right == 0:\n raise self._error(\n node,\n \"Тақсим ба сифр мумкин нест.\",\n hint=\"Тақсимкунанда набояд 0 бошад.\",\n kind_code=KIND_DIVISION,\n )\n if op == \"%\":\n return number(left % right)\n # `/` always produces a fraction, so both sides become Decimal\n return number(Decimal(left) / Decimal(right))\n\n raise self._error(node, f'Оператори нофаҳмо: \"{op}\".')\n\n def _order(self, node: ast.Node, op: str, left: Any, right: Any) -> bool:\n \"\"\"< > <= >= — ordering, which unlike equality can fail.\n\n Numbers compare numerically. Strings compare by the **Tajik alphabet**\n (see collation.py), not by Unicode code point, so `\"Ғафуров\" < \"Яқубов\"`\n is рост the way a Tajik reader expects.\n \"\"\"\n if is_number(left) and is_number(right):\n result = (left > right) - (left < right)\n elif isinstance(left, str) and isinstance(right, str):\n result = collation.compare(left, right)\n else:\n raise self._error(\n node,\n f'Амали \"{op}\" байни {type_name(left)} ва {type_name(right)} '\n \"иҷро намешавад.\",\n hint=f'Амали \"{op}\" ҳарду рақам ё ҳарду матн мехоҳад.',\n )\n\n if op == \"<\":\n return result < 0\n if op == \">\":\n return result > 0\n if op == \"<=\":\n return result <= 0\n return result >= 0\n\n def _suggest(self, name: str) -> str:\n visible = self.environment.visible_names()\n matches = difflib.get_close_matches(name, visible, n=1)\n if matches:\n return f'Оё шумо \"{matches[0]}\" -ро дар назар доштед?'\n return \"Номҳои мавҷуда: \" + \", \".join(sorted(visible))\n", "lexer.py": "\"\"\"Лексер — characters in, tokens out.\n\n бигзор калон = синну_сол >= 18\n\nbecomes\n\n LET IDENT('калон') ASSIGN IDENT('синну_сол') GTE NUMBER(18) NEWLINE EOF\n\nThe lexer knows nothing about what a program *means*. It only recognises\nshapes: this run of letters is a name, this run of digits is a number, this\nquoted run is a string.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport unicodedata\nfrom decimal import Decimal\n\nfrom .errors import LexerError\nfrom .syntax_next import normalize as normalize_next_syntax\nfrom .tokens import (\n KEYWORD_TOKENS,\n KEYWORD_VERSIONS,\n KEYWORDS,\n SINGLE_CHARS,\n TWO_CHAR_OPERATORS,\n WRONG_OPERATORS,\n Token,\n TokenType,\n)\n\n# Written as chr(92) so that this source file contains no backslash puzzles of\n# its own while describing the escape sequences of another language.\n_B = chr(92)\n\n# Escape sequences allowed inside \"...\"\nESCAPES = {\n \"n\": \"\\n\",\n \"t\": \"\\t\",\n \"r\": \"\\r\", # real files arrive with CRLF; a program should be able\n '\"': '\"', # to say so\n _B: _B,\n}\n\nESCAPE_HINT = (\n \"Танҳо \" + _B + \"n, \" + _B + \"t, \" + _B + \"r, \" + _B + '\" ва ' + _B + _B\n + \" иҷозат дода мешаванд.\"\n)\n\n# One block level is exactly this many spaces. See LANGUAGE_SPEC.md §2.7 for\n# why the width is fixed rather than merely consistent.\nINDENT_WIDTH = 4\n\n\nclass Lexer:\n def __init__(self, source: str, filename: str = \"<матн>\") -> None:\n # Normalise to NFC before anything else.\n #\n # Tajik letters such as ӣ and ӯ can be typed two different ways: as one\n # code point (U+04E3), or as и + a combining macron (U+0438 U+0304).\n # They look identical on screen. Without this line, `ҳарорат` typed on\n # one keyboard would be a *different variable* from `ҳарорат` typed on\n # another, and nobody would ever work out why.\n try:\n source = normalize_next_syntax(source)\n except ValueError as error:\n raise LexerError(str(error), 1, 1, source.splitlines()[0] if source else \"\") from error\n self.source = unicodedata.normalize(\"NFC\", source)\n self.filename = filename\n self.lines = self.source.splitlines()\n\n self.pos = 0 # index into self.source\n self.line = 1 # 1-based\n self.column = 1 # 1-based\n\n # The indentation stack: the width of every block currently open.\n # [0] means \"at the outermost level\". Pushing is an INDENT, popping is\n # a DEDENT. This is the whole of block structure, in one list.\n self.indents = [0]\n\n # How many brackets are open. While any are, newlines and indentation\n # carry no meaning — see the newline rule in tokenize().\n self.bracket_depth = 0\n\n # ------------------------------------------------------------------\n # low-level cursor helpers\n # ------------------------------------------------------------------\n def _at_end(self) -> bool:\n return self.pos >= len(self.source)\n\n def _peek(self, offset: int = 0) -> str:\n index = self.pos + offset\n if index >= len(self.source):\n return \"\"\n return self.source[index]\n\n def _advance(self) -> str:\n char = self.source[self.pos]\n self.pos += 1\n if char == \"\\n\":\n self.line += 1\n self.column = 1\n else:\n self.column += 1\n return char\n\n def _source_line(self, line: int) -> str:\n if 1 <= line <= len(self.lines):\n return self.lines[line - 1]\n return \"\"\n\n def _error(self, message: str, line: int, column: int, hint: str = \"\") -> LexerError:\n return LexerError(message, line, column, self._source_line(line), hint)\n\n def _wrong_operator(self, text: str, line: int, column: int) -> LexerError:\n for _ in text:\n self._advance()\n return self._error(\n f'Оператори \"{text}\" дар TajikLang вуҷуд надорад.',\n line,\n column,\n hint=WRONG_OPERATORS[text],\n )\n\n # ------------------------------------------------------------------\n # public API\n # ------------------------------------------------------------------\n def tokenize(self) -> list[Token]:\n tokens: list[Token] = []\n\n # Indentation is only meaningful at the start of a line, so it is\n # handled there and nowhere else.\n self._start_of_line(tokens)\n\n while not self._at_end():\n char = self._peek()\n line, column = self.line, self.column\n\n # Inside a line, spaces and tabs only separate tokens.\n if char in \" \\t\\r\":\n self._advance()\n continue\n\n # comments run to the end of the line\n if char == \"#\":\n while not self._at_end() and self._peek() != \"\\n\":\n self._advance()\n continue\n\n if char == \"\\n\":\n self._advance()\n\n # Implicit line joining: a newline inside (), [] or {} is just\n # whitespace. Without this a table of data has to be written on\n # one enormous line, which is exactly where a student most\n # wants to break it up.\n if self.bracket_depth > 0:\n continue\n\n tokens.append(Token(TokenType.NEWLINE, \"newline\", line, column))\n self._start_of_line(tokens)\n continue\n\n # Operator matching, longest first, alternating between real\n # operators and the ones students bring from other languages.\n #\n # The order matters: \"===\" must be caught before \"==\" leaves a\n # stray \"=\" behind, but \"!=\" must be matched before the lone \"!\"\n # rule fires, or a valid comparison would be reported as a mistake.\n three = char + self._peek(1) + self._peek(2)\n two = char + self._peek(1)\n\n if three in WRONG_OPERATORS:\n raise self._wrong_operator(three, line, column)\n\n if two in TWO_CHAR_OPERATORS:\n self._advance()\n self._advance()\n tokens.append(Token(TWO_CHAR_OPERATORS[two], two, line, column))\n continue\n\n if two in WRONG_OPERATORS:\n raise self._wrong_operator(two, line, column)\n\n if char in WRONG_OPERATORS:\n raise self._wrong_operator(char, line, column)\n\n if char in SINGLE_CHARS:\n self._advance()\n if char in \"([{\":\n self.bracket_depth += 1\n elif char in \")]}\":\n self.bracket_depth = max(0, self.bracket_depth - 1)\n tokens.append(Token(SINGLE_CHARS[char], char, line, column))\n continue\n\n if char == '\"':\n tokens.append(self._read_string())\n continue\n\n if char.isdigit():\n tokens.append(self._read_number())\n continue\n\n if char.isalpha() or char == \"_\":\n tokens.append(self._read_identifier())\n continue\n\n self._advance()\n raise self._error(\n f'Рамзи ношинос: \"{char}\".',\n line,\n column,\n hint=\"Шояд ин рамз тасодуфан навишта шудааст.\",\n )\n\n # A file that does not end in a newline still ends a statement.\n if tokens and tokens[-1].type is not TokenType.NEWLINE:\n tokens.append(Token(TokenType.NEWLINE, \"newline\", self.line, self.column))\n\n # Every block still open at the end of the file closes here, so the\n # parser never has to treat \"ran out of file\" as a special case.\n while len(self.indents) > 1:\n self.indents.pop()\n tokens.append(Token(TokenType.DEDENT, 0, self.line, self.column))\n\n tokens.append(Token(TokenType.EOF, None, self.line, self.column))\n return tokens\n\n # ------------------------------------------------------------------\n # indentation\n # ------------------------------------------------------------------\n def _start_of_line(self, tokens: list[Token]) -> None:\n \"\"\"Measure the indentation of the line we are about to read.\n\n Emits INDENT when a block opens and one DEDENT per block that closes.\n Blank lines and comment-only lines are skipped entirely: their\n indentation means nothing, and treating it as meaningful is a classic\n way to make a lexer reject perfectly good files.\n \"\"\"\n line, column = self.line, self.column\n width = 0\n\n while True:\n char = self._peek()\n if char == \" \":\n width += 1\n self._advance()\n elif char == \"\\t\":\n tab_line, tab_column = self.line, self.column\n self._advance()\n raise self._error(\n \"Дар фосилагузорӣ рамзи tab истифода шудааст.\",\n tab_line,\n tab_column,\n hint=f\"Танҳо холигӣ (space) истифода баред — {INDENT_WIDTH} \"\n \"холигӣ барои ҳар сатҳ.\",\n )\n else:\n break\n\n # blank line, comment-only line, or end of file\n if self._at_end() or self._peek() in (\"\\n\", \"\\r\", \"#\"):\n return\n\n current = self.indents[-1]\n\n if width == current:\n return\n\n if width > current:\n if width != current + INDENT_WIDTH:\n raise self._error(\n f\"Фосилаи нодуруст: {width} холигӣ.\",\n line,\n width + 1, # under the first real character of the line\n hint=f\"Ҳар сатҳи нав бояд маҳз {current + INDENT_WIDTH} \"\n \"холигӣ дошта бошад.\",\n )\n self.indents.append(width)\n tokens.append(Token(TokenType.INDENT, width, line, column))\n return\n\n # width < current: close every block deeper than this line\n while len(self.indents) > 1 and width < self.indents[-1]:\n self.indents.pop()\n tokens.append(Token(TokenType.DEDENT, width, line, column))\n\n if width != self.indents[-1]:\n raise self._error(\n f\"Фосилаи {width} холигӣ ба ҳеҷ блоки кушода мувофиқ намеояд.\",\n line,\n width + 1, # under the first real character of the line\n hint=\"Сатҳҳои кушода: \"\n + \", \".join(str(level) for level in self.indents),\n )\n\n # ------------------------------------------------------------------\n # token readers\n # ------------------------------------------------------------------\n def _read_string(self) -> Token:\n line, column = self.line, self.column\n self._advance() # consume the opening quote\n\n chars: list[str] = []\n while True:\n if self._at_end() or self._peek() == \"\\n\":\n raise self._error(\n \"Сатри матн пӯшида нашудааст.\",\n line,\n column,\n hint='Дар охири матн аломати \" -ро илова кунед.',\n )\n\n char = self._advance()\n\n if char == '\"':\n return Token(TokenType.STRING, \"\".join(chars), line, column)\n\n if char == _B:\n esc_line, esc_column = self.line, self.column\n if self._at_end():\n continue # the unterminated-string check above will fire\n escape = self._advance()\n if escape not in ESCAPES:\n raise self._error(\n 'Рамзи гурезии номаълум: \"' + _B + escape + '\".',\n esc_line,\n esc_column - 1,\n hint=ESCAPE_HINT,\n )\n chars.append(ESCAPES[escape])\n continue\n\n chars.append(char)\n\n def _read_number(self) -> Token:\n line, column = self.line, self.column\n digits: list[str] = []\n\n while not self._at_end() and self._peek().isdigit():\n digits.append(self._advance())\n\n # a decimal point only counts if a digit follows it\n if self._peek() == \".\" and self._peek(1).isdigit():\n digits.append(self._advance())\n while not self._at_end() and self._peek().isdigit():\n digits.append(self._advance())\n # Decimal, not float: see the note at the top of values.py.\n return Token(\n TokenType.NUMBER, Decimal(\"\".join(digits)), line, column\n )\n\n return Token(TokenType.NUMBER, int(\"\".join(digits)), line, column)\n\n def _read_identifier(self) -> Token:\n line, column = self.line, self.column\n chars: list[str] = []\n\n # `isalnum()` is true for Cyrillic letters and for Tajik-specific ones\n # such as ӣ ӯ қ ғ ҳ ҷ, so identifiers like `шаҳр` need no special case.\n while not self._at_end() and (self._peek().isalnum() or self._peek() == \"_\"):\n chars.append(self._advance())\n\n name = \"\".join(chars)\n\n if name in KEYWORD_TOKENS:\n return Token(KEYWORD_TOKENS[name], name, line, column)\n\n if name in KEYWORDS:\n version = KEYWORD_VERSIONS.get(name, \"оянда\")\n raise self._error(\n f'Калимаи калидии \"{name}\" ҳанӯз дастгирӣ намешавад.',\n line,\n column,\n hint=f\"Ин калима барои версияи {version} захира шудааст.\",\n )\n\n return Token(TokenType.IDENT, name, line, column)\n", "packages.py": "\"\"\"Бастаҳо — TajikLang's own package manager.\n\n tajik гирифтан омор # install a package\n tajik бастаҳо # list what is installed\n tajik нест омор # remove one\n tajik ҷустуҷӯ # see what exists\n\nDeliberately **not** pip. A student installing a TajikLang library should not\nhave to learn Python's tooling first, and a school that runs the standalone\nbuild has no pip at all. So packages are plain folders of `.tj` files, the\nregistry is one JSON file, and everything here uses the standard library.\n\nWhere things live:\n\n %LOCALAPPDATA%\\\\TajikLang\\\\ (Windows)\n ~/.tajiklang/ (everywhere else)\n китобхона/ installed packages\n омор/\n омор.tj the file `ворид омор` finds\n баста.json what was installed, and from where\n\nSet `TAJIKLANG_HOME` to move the whole thing — a school can put it on a shared\ndrive, or a student on a USB stick.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport io\nimport json\nimport os\nimport shutil\nimport urllib.error\nimport urllib.request\nimport zipfile\nfrom pathlib import Path\nfrom typing import Any\n\n# The registry that ships with the language, so `tajik ҷустуҷӯ` works offline.\nBUNDLED_REGISTRY = Path(__file__).resolve().parent.parent / \"бастаҳо\" / \"феҳрист.json\"\n\n# The same file on GitHub, so a new package appears without a new release.\nREMOTE_REGISTRY = (\n \"https://raw.githubusercontent.com/BahromPython/tajiklang/main/\"\n \"%D0%B1%D0%B0%D1%81%D1%82%D0%B0%D2%B3%D0%BE/\"\n \"%D1%84%D0%B5%D2%B3%D1%80%D0%B8%D1%81%D1%82.json\"\n)\n\nTIMEOUT = 20\n\n\nclass PackageError(Exception):\n \"\"\"Something went wrong that the student should read, in Tajik.\"\"\"\n\n def __init__(self, message: str, hint: str = \"\") -> None:\n super().__init__(message)\n self.message = message\n self.hint = hint\n\n\n# ---------------------------------------------------------------------------\n# where things live\n# ---------------------------------------------------------------------------\ndef home() -> Path:\n \"\"\"The folder holding installed packages.\"\"\"\n override = os.environ.get(\"TAJIKLANG_HOME\")\n if override:\n return Path(override)\n if os.name == \"nt\":\n base = os.environ.get(\"LOCALAPPDATA\") or str(Path.home())\n return Path(base) / \"TajikLang\"\n return Path.home() / \".tajiklang\"\n\n\ndef library(project_root: Path | None = None) -> Path:\n \"\"\"The global library, or a project's own library when requested.\"\"\"\n if project_root is not None:\n from . import environment\n\n return project_root / environment.ENV_DIR / environment.LIBRARY\n return home() / \"китобхона\"\n\n\ndef installed(project_root: Path | None = None) -> dict[str, dict[str, Any]]:\n \"\"\"Every installed package, by name.\"\"\"\n found: dict[str, dict[str, Any]] = {}\n folder = library(project_root)\n if not folder.is_dir():\n return found\n\n for entry in sorted(folder.iterdir()):\n metadata = entry / \"баста.json\"\n if entry.is_dir() and metadata.is_file():\n try:\n found[entry.name] = json.loads(metadata.read_text(encoding=\"utf-8\"))\n except json.JSONDecodeError:\n found[entry.name] = {\"ном\": entry.name, \"нусха\": \"?\"}\n return found\n\n\ndef resolve(name: str, base_dir: Path | None = None) -> Path | None:\n \"\"\"Find a package: project environment first, global library second.\"\"\"\n if base_dir is not None:\n from . import environment\n\n project = environment.find(base_dir)\n if project is not None:\n candidate = library(project) / name / f\"{name}.tj\"\n if candidate.is_file():\n return candidate\n\n candidate = library() / name / f\"{name}.tj\"\n return candidate if candidate.is_file() else None\n\n\n# ---------------------------------------------------------------------------\n# the registry\n# ---------------------------------------------------------------------------\ndef registry(offline: bool = False) -> dict[str, Any]:\n \"\"\"The catalogue of packages — fetched if possible, bundled if not.\"\"\"\n if not offline:\n try:\n with urllib.request.urlopen(REMOTE_REGISTRY, timeout=TIMEOUT) as response:\n return json.loads(response.read().decode(\"utf-8\"))\n except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError):\n pass # no network is normal in a classroom; fall through\n\n if BUNDLED_REGISTRY.is_file():\n return json.loads(BUNDLED_REGISTRY.read_text(encoding=\"utf-8\"))\n return {\"бастаҳо\": {}}\n\n\ndef catalogue(offline: bool = False) -> dict[str, dict[str, Any]]:\n return registry(offline).get(\"бастаҳо\", {})\n\n\n# ---------------------------------------------------------------------------\n# installing\n# ---------------------------------------------------------------------------\ndef install(\n name: str, offline: bool = False, project_root: Path | None = None\n) -> dict[str, Any]:\n \"\"\"Download a package and put its `.tj` files where `ворид` will find them.\"\"\"\n entries = catalogue(offline)\n if name not in entries:\n near = \", \".join(sorted(entries)) or \"ҳеҷ\"\n raise PackageError(\n f'Бастаи \"{name}\" дар феҳрист нест.',\n hint=f\"Бастаҳои мавҷуда: {near}. Барои дидан: tajik ҷустуҷӯ\",\n )\n\n entry = entries[name]\n url = entry.get(\"манбаъ\")\n if not url:\n raise PackageError(f'Бастаи \"{name}\" суроғаи боргирӣ надорад.')\n\n try:\n with urllib.request.urlopen(url, timeout=TIMEOUT) as response:\n payload = response.read()\n except (urllib.error.URLError, TimeoutError, OSError) as error:\n raise PackageError(\n f'Бастаи \"{name}\" боргирӣ нашуд.',\n hint=f\"Пайвасти интернетро санҷед. ({error})\",\n ) from None\n\n target = library(project_root) / name\n if target.exists():\n shutil.rmtree(target)\n target.mkdir(parents=True)\n\n inside = entry.get(\"ҷузвдон\", \"\") # a package may live inside a monorepo\n count = _extract(payload, target, inside, name)\n\n if count == 0:\n shutil.rmtree(target)\n raise PackageError(\n f'Дар бастаи \"{name}\" ягон файли .tj нест.',\n hint=\"Шояд суроғаи баста нодуруст аст.\",\n )\n if not (target / f\"{name}.tj\").is_file():\n shutil.rmtree(target)\n raise PackageError(\n f'Бастаи \"{name}\" файли асосии \"{name}.tj\" -ро надорад.',\n hint=\"Ҳар баста бояд файле бо номи худаш дошта бошад.\",\n )\n\n metadata = {\n \"ном\": name,\n \"нусха\": entry.get(\"нусха\", \"?\"),\n \"тавсиф\": entry.get(\"тавсиф\", \"\"),\n \"муаллиф\": entry.get(\"муаллиф\", \"\"),\n \"манбаъ\": url,\n \"файлҳо\": count,\n }\n (target / \"баста.json\").write_text(\n json.dumps(metadata, ensure_ascii=False, indent=2), encoding=\"utf-8\"\n )\n return metadata\n\n\ndef _extract(payload: bytes, target: Path, inside: str, name: str) -> int:\n \"\"\"Pull the `.tj` files out of a downloaded zip, flat, and nothing else.\n\n Only `.tj` files are taken, and every path is checked before it is used:\n a zip that says `../../evil.tj` must not be able to write outside the\n package folder.\n \"\"\"\n written = 0\n with zipfile.ZipFile(io.BytesIO(payload)) as archive:\n for item in archive.infolist():\n if item.is_dir() or not item.filename.endswith(\".tj\"):\n continue\n\n path = item.filename.replace(\"\\\\\", \"/\")\n if inside and f\"/{inside}/\" not in f\"/{path}\":\n continue\n\n leaf = Path(path).name\n if not leaf or leaf.startswith(\".\") or \"/\" in leaf or \"..\" in leaf:\n continue\n\n destination = (target / leaf).resolve()\n if not str(destination).startswith(str(target.resolve())):\n continue # a path trying to escape the folder\n\n destination.write_bytes(archive.read(item))\n written += 1\n return written\n\n\ndef remove(name: str, project_root: Path | None = None) -> None:\n target = library(project_root) / name\n if not target.is_dir():\n raise PackageError(\n f'Бастаи \"{name}\" насб нашудааст.',\n hint=\"Барои дидани бастаҳо: tajik бастаҳо\",\n )\n shutil.rmtree(target)\n", "page.py": "\"\"\"Саҳифа — building web pages from TajikLang.\n\nThe idea that makes this small instead of enormous: an HTML page is a **tree**,\nand TajikLang already has trees. So `саҳифа` does not invent a markup syntax,\na template language, or a compiler to JavaScript. It builds a tree of values:\n\n ворид саҳифа\n\n саҳифа.нависед(\n саҳифа.сарлавҳа(\"Журнали синфи 10\"),\n саҳифа.ҷадвал(сатрҳо, [\"Ном\", \"Баҳо\"]),\n саҳифа.тугма(\"Аз нав ҳисоб кун\", ҳисоб_кун),\n )\n\nBecause the tree is plain data, the *same program* has two destinations:\n\n* in a browser (the playground) it becomes real DOM, with `тугма` handlers\n wired to ordinary TajikLang functions;\n* in the terminal `саҳифа.ҳамчун_матн(...)` returns the HTML as text, which\n `файл.навиштан` saves — so a student can generate a real web page from the\n command line with no browser at all.\n\nStyling is a dictionary, with Tajik names for the properties a beginner\nactually reaches for. Unknown names pass through untouched, so anyone who\nknows CSS is not fenced in.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom decimal import Decimal\nfrom typing import Any\n\nfrom .errors import BuiltinError\nfrom .values import Builtin, to_tajik_string, type_name\n\n# Tajik name -> CSS property. Anything not listed passes through as written,\n# so knowing CSS is a bonus rather than a requirement.\nSTYLE_NAMES = {\n \"ранг\": \"color\",\n \"замина\": \"background\",\n \"андоза\": \"font-size\",\n \"фосила\": \"padding\",\n \"ҳошия\": \"margin\",\n \"васеъӣ\": \"width\",\n \"баландӣ\": \"height\",\n \"канор\": \"border\",\n \"гӯшаҳо\": \"border-radius\",\n \"ҳарф\": \"font-family\",\n \"ғафсӣ\": \"font-weight\",\n \"ҷойгиршавӣ\": \"text-align\",\n}\n\n# Properties whose bare numbers mean pixels.\nPIXEL_PROPERTIES = {\n \"font-size\", \"padding\", \"margin\", \"width\", \"height\", \"border-radius\",\n}\n\nVOID_TAGS = {\"img\", \"input\", \"br\", \"hr\"}\n\n\ndef escape(text: str) -> str:\n \"\"\"Text going into HTML. Without this a program could inject markup.\"\"\"\n return (\n text.replace(\"&\", \"&\")\n .replace(\"<\", \"<\")\n .replace(\">\", \">\")\n .replace('\"', \""\")\n )\n\n\nclass Element:\n \"\"\"One node of the page. Plain data — no browser needed to build it.\"\"\"\n\n def __init__(\n self,\n tag: str,\n text: str | None = None,\n children: list[\"Element\"] | None = None,\n attributes: dict[str, str] | None = None,\n style: dict[str, str] | None = None,\n handler: Any = None,\n ) -> None:\n self.tag = tag\n self.text = text\n self.children = children or []\n self.attributes = attributes or {}\n self.style = style or {}\n self.handler = handler\n\n def __repr__(self) -> str:\n return f\"<унсури {self.tag}>\"\n\n # ------------------------------------------------------------------\n def style_text(self) -> str:\n return \"; \".join(f\"{name}: {value}\" for name, value in self.style.items())\n\n def to_html(self, indent: int = 0, interactive: bool = False) -> str:\n pad = \" \" * indent\n attributes = dict(self.attributes)\n if not interactive:\n # A static site must stay a normal HTML file. TajikWeb App adds\n # this private attribute only while a live interpreter exists.\n attributes.pop(\"data-tajik-action\", None)\n if self.style:\n attributes[\"style\"] = self.style_text()\n\n rendered = \"\".join(\n f' {name}=\"{escape(str(value))}\"' for name, value in attributes.items()\n )\n\n if self.tag in VOID_TAGS:\n return f\"{pad}<{self.tag}{rendered}>\"\n\n inner = escape(self.text) if self.text is not None else \"\"\n if not self.children:\n return f\"{pad}<{self.tag}{rendered}>{inner}\"\n\n lines = [f\"{pad}<{self.tag}{rendered}>\"]\n if inner:\n lines.append(\" \" * (indent + 1) + inner)\n for child in self.children:\n lines.append(child.to_html(indent + 1, interactive))\n lines.append(f\"{pad}\")\n return chr(10).join(lines)\n\n\nDOCUMENT = \"\"\"\n\n\n\n\n{title}\n\n\n\n{body}\n\n\n\"\"\"\n\n\ndef _text_of(value: Any) -> str:\n return to_tajik_string(value)\n\n\ndef _elements(name: str, values: tuple[Any, ...]) -> list[Element]:\n \"\"\"Arguments that should each be an element — with plain text allowed.\"\"\"\n result: list[Element] = []\n for value in values:\n if isinstance(value, Element):\n result.append(value)\n elif isinstance(value, (str, int, Decimal)) or value is None:\n result.append(Element(\"p\", text=_text_of(value)))\n elif isinstance(value, list):\n result.extend(_elements(name, tuple(value)))\n else:\n raise BuiltinError(\n f'Функсияи \"саҳифа.{name}\" унсур ё матн мехоҳад, вале '\n f\"{type_name(value)} гирифт.\",\n hint=\"Унсурҳоро бо саҳифа.сарлавҳа, саҳифа.матн ва ғайра созед.\",\n )\n return result\n\n\ndef _style(name: str, value: Any) -> dict[str, str]:\n if value is None:\n return {}\n if not isinstance(value, dict):\n raise BuiltinError(\n f'Услуби \"саҳифа.{name}\" бояд луғат бошад, вале '\n f\"{type_name(value)} аст.\",\n hint='Масалан: {\"ранг\": \"кабуд\", \"андоза\": 20}',\n )\n\n style: dict[str, str] = {}\n for key, raw in value.items():\n if not isinstance(key, str):\n raise BuiltinError(\"Номи услуб бояд матн бошад.\")\n css = STYLE_NAMES.get(key, key)\n if isinstance(raw, (int, Decimal)) and not isinstance(raw, bool):\n style[css] = f\"{to_tajik_string(raw)}px\" if css in PIXEL_PROPERTIES \\\n else to_tajik_string(raw)\n else:\n style[css] = to_tajik_string(raw)\n return style\n\n\ndef build_module(interpreter: Any) -> dict[str, Any]:\n \"\"\"The `саҳифа` module. `interpreter` is needed to call click handlers.\"\"\"\n\n # What `нависед` produced, kept so the terminal can print or save it.\n written: list[Element] = []\n # TajikWeb App keeps the interpreter alive and calls these ordinary\n # TajikLang functions when a browser presses a button.\n actions: dict[str, Any] = {}\n action_number = 0\n\n # --- building blocks ---------------------------------------------------\n def сарлавҳа(*args: Any) -> Element:\n if not 1 <= len(args) <= 3:\n raise BuiltinError('Функсияи \"саҳифа.сарлавҳа\" 1-3 аргумент мехоҳад.')\n level = 1\n style = None\n if len(args) >= 2 and isinstance(args[1], (int, Decimal)):\n level = max(1, min(3, int(args[1])))\n style = args[2] if len(args) == 3 else None\n elif len(args) >= 2:\n style = args[1]\n return Element(\n f\"h{level}\", text=_text_of(args[0]), style=_style(\"сарлавҳа\", style)\n )\n\n def матн(*args: Any) -> Element:\n if not args:\n raise BuiltinError('Функсияи \"саҳифа.матн\" ҳадди аққал 1 аргумент мехоҳад.')\n style = None\n values = args\n if isinstance(args[-1], dict):\n values, style = args[:-1], args[-1]\n return Element(\n \"p\",\n text=\" \".join(_text_of(v) for v in values),\n style=_style(\"матн\", style),\n )\n\n def қуттӣ(*args: Any) -> Element:\n \"\"\"Унсурҳоро зери ҳам мегузорад.\"\"\"\n values, style = _split_style(args)\n return Element(\"div\", children=_elements(\"қуттӣ\", values),\n style=_style(\"қуттӣ\", style))\n\n def қатор(*args: Any) -> Element:\n \"\"\"Унсурҳоро паҳлӯи ҳам мегузорад.\"\"\"\n values, style = _split_style(args)\n base = {\"display\": \"flex\", \"gap\": \"12px\", \"align-items\": \"center\"}\n base.update(_style(\"қатор\", style))\n return Element(\"div\", children=_elements(\"қатор\", values), style=base)\n\n def _split_style(args: tuple[Any, ...]) -> tuple[tuple[Any, ...], Any]:\n if args and isinstance(args[-1], dict):\n return args[:-1], args[-1]\n return args, None\n\n def рӯйхат(*args: Any) -> Element:\n if not 1 <= len(args) <= 2:\n raise BuiltinError('Функсияи \"саҳифа.рӯйхат\" 1 ё 2 аргумент мехоҳад.')\n items = args[0]\n if not isinstance(items, list):\n raise BuiltinError(\n f'Функсияи \"саҳифа.рӯйхат\" рӯйхат мехоҳад, вале '\n f\"{type_name(items)} гирифт.\"\n )\n children = [Element(\"li\", text=_text_of(item)) for item in items]\n style = args[1] if len(args) == 2 else None\n return Element(\"ul\", children=children, style=_style(\"рӯйхат\", style))\n\n def ҷадвал(*args: Any) -> Element:\n \"\"\"ҷадвал(сатрҳо, [сарлавҳаҳо]) — сатрҳо рӯйхати рӯйхатҳо ё луғатҳо.\"\"\"\n if not 1 <= len(args) <= 2:\n raise BuiltinError('Функсияи \"саҳифа.ҷадвал\" 1 ё 2 аргумент мехоҳад.')\n rows = args[0]\n if not isinstance(rows, list):\n raise BuiltinError(\n f'Функсияи \"саҳифа.ҷадвал\" рӯйхат мехоҳад, вале '\n f\"{type_name(rows)} гирифт.\"\n )\n headers = args[1] if len(args) == 2 else None\n\n # a list of dictionaries: the headers name the columns to take\n if rows and isinstance(rows[0], dict):\n if headers is None:\n headers = list(rows[0])\n body = [[row.get(column) for column in headers] for row in rows]\n else:\n body = [row if isinstance(row, list) else [row] for row in rows]\n\n children: list[Element] = []\n if headers:\n children.append(\n Element(\"tr\", children=[\n Element(\"th\", text=_text_of(h)) for h in headers\n ])\n )\n for row in body:\n children.append(\n Element(\"tr\", children=[\n Element(\"td\", text=_text_of(cell)) for cell in row\n ])\n )\n return Element(\"table\", children=children)\n\n def тугма(*args: Any) -> Element:\n \"\"\"тугма(матн, функсия) — функсия ҳангоми пахш даъват мешавад.\"\"\"\n if not 2 <= len(args) <= 3:\n raise BuiltinError('Функсияи \"саҳифа.тугма\" 2 ё 3 аргумент мехоҳад.')\n handler = args[1]\n if not callable(handler) and type_name(handler) != \"функсия\":\n raise BuiltinError(\n f'Функсияи \"саҳифа.тугма\" ҳамчун аргументи дуюм функсия '\n f\"мехоҳад, вале {type_name(handler)} гирифт.\",\n hint=\"Аввал функсия эълон кунед, баъд номи онро диҳед.\",\n )\n style = args[2] if len(args) == 3 else None\n nonlocal action_number\n action_number += 1\n action_name = f\"амал_{action_number}\"\n actions[action_name] = handler\n interpreter.page_actions = actions\n return Element(\n \"button\", text=_text_of(args[0]), handler=handler,\n attributes={\"data-tajik-action\": action_name},\n style=_style(\"тугма\", style),\n )\n\n def пайванд(*args: Any) -> Element:\n \"\"\"пайванд(матн, суроға, [услуб]) — a normal portable web link.\"\"\"\n if not 2 <= len(args) <= 3:\n raise BuiltinError('Функсияи \"саҳифа.пайванд\" 2 ё 3 аргумент мехоҳад.')\n style = args[2] if len(args) == 3 else None\n return Element(\n \"a\", text=_text_of(args[0]), attributes={\"href\": _text_of(args[1])},\n style=_style(\"пайванд\", style),\n )\n\n def майдон(*args: Any) -> Element:\n \"\"\"майдон(ном, [пешфарз]) — ҷои воридкунии матн.\"\"\"\n if not 1 <= len(args) <= 2:\n raise BuiltinError('Функсияи \"саҳифа.майдон\" 1 ё 2 аргумент мехоҳад.')\n name = _text_of(args[0])\n attributes = {\"data-ном\": name}\n if len(args) == 2:\n attributes[\"value\"] = _text_of(args[1])\n return Element(\"input\", attributes=attributes)\n\n def расм(*args: Any) -> Element:\n if not 1 <= len(args) <= 2:\n raise BuiltinError('Функсияи \"саҳифа.расм\" 1 ё 2 аргумент мехоҳад.')\n attributes = {\"src\": _text_of(args[0])}\n attributes[\"alt\"] = _text_of(args[1]) if len(args) == 2 else \"\"\n return Element(\"img\", attributes=attributes)\n\n def услуб(*args: Any) -> Element:\n \"\"\"Ба унсури тайёр услуб илова мекунад.\"\"\"\n if len(args) != 2 or not isinstance(args[0], Element):\n raise BuiltinError(\n 'Функсияи \"саҳифа.услуб\" унсур ва луғати услуб мехоҳад.',\n hint='Масалан: саҳифа.услуб(сарлавҳа, {\"ранг\": \"кабуд\"})',\n )\n element = args[0]\n element.style.update(_style(\"услуб\", args[1]))\n return element\n\n # --- destinations ------------------------------------------------------\n def _as_html(elements: list[Element], interactive: bool = False) -> str:\n body = chr(10).join(element.to_html(0, interactive) for element in elements)\n title = \"TajikLang\"\n for element in elements:\n if element.tag.startswith(\"h\") and element.text:\n title = element.text\n break\n return DOCUMENT.format(title=escape(title), body=body)\n\n def ҳамчун_матн(*args: Any) -> str:\n \"\"\"Саҳифаро ҳамчун матни HTML бармегардонад.\"\"\"\n elements = _elements(\"ҳамчун_матн\", args) if args else list(written)\n return _as_html(elements)\n\n # A deliberately private bridge for TajikWeb App. It is set on the\n # interpreter, not exposed as a new English-looking language feature.\n interpreter.page_html = lambda: _as_html(list(written), interactive=True)\n\n def нависед(*args: Any) -> None:\n \"\"\"Унсурҳоро ба саҳифа мегузорад (дар браузер) ё нигоҳ медорад.\"\"\"\n elements = _elements(\"нависед\", args)\n written.extend(elements)\n _render(elements)\n\n def тоза_кун(*args: Any) -> None:\n written.clear()\n target = _target(required=False)\n if target is not None:\n target.innerHTML = \"\"\n\n def қимат(*args: Any) -> str:\n \"\"\"Он чи корбар ба майдон навиштааст.\"\"\"\n if len(args) != 1:\n raise BuiltinError('Функсияи \"саҳифа.қимат\" 1 аргумент мехоҳад.')\n document = _document(\n 'Функсияи \"саҳифа.қимат\" танҳо дар браузер кор мекунад.'\n )\n name = _text_of(args[0])\n found = document.querySelector('[data-ном=\"' + name + '\"]')\n if found is None:\n raise BuiltinError(\n f'Майдони \"{name}\" дар саҳифа нест.',\n hint=\"Аввал онро созед: саҳифа.майдон(«ном»)\",\n )\n return str(found.value)\n\n # --- browser plumbing --------------------------------------------------\n def _document(message: str) -> Any:\n try:\n from js import document # type: ignore\n except ImportError:\n raise BuiltinError(\n message,\n hint=\"Дар терминал «саҳифа.ҳамчун_матн» -ро истифода баред ва \"\n \"натиҷаро бо «файл.навиштан» нигоҳ доред.\",\n ) from None\n return document\n\n def _target(required: bool = True) -> Any:\n try:\n from js import document # type: ignore\n except ImportError:\n if required:\n raise BuiltinError(\n 'Функсияи \"саҳифа.нависед\" танҳо дар браузер кор мекунад.',\n hint=\"Дар терминал «саҳифа.ҳамчун_матн» -ро истифода баред.\",\n ) from None\n return None\n return document.getElementById(\"tajik-page\") or document.body\n\n def _render(elements: list[Element]) -> None:\n target = _target(required=False)\n if target is None:\n return # terminal: kept for ҳамчун_матн\n for element in elements:\n target.appendChild(_to_dom(element))\n\n def _to_dom(element: Element) -> Any:\n from js import document # type: ignore\n\n node = document.createElement(element.tag)\n for name, value in element.attributes.items():\n node.setAttribute(name, str(value))\n if element.style:\n node.setAttribute(\"style\", element.style_text())\n if element.text is not None:\n node.textContent = element.text\n for child in element.children:\n node.appendChild(_to_dom(child))\n\n if element.handler is not None:\n from pyodide.ffi import create_proxy # type: ignore\n\n handler = element.handler\n\n def clicked(_event: Any) -> None:\n interpreter.call_from_host(handler, [])\n\n node.addEventListener(\"click\", create_proxy(clicked))\n\n return node\n\n names = {\n \"сарлавҳа\": сарлавҳа, \"матн\": матн, \"қуттӣ\": қуттӣ, \"қатор\": қатор,\n \"рӯйхат\": рӯйхат, \"ҷадвал\": ҷадвал, \"тугма\": тугма, \"пайванд\": пайванд, \"майдон\": майдон,\n \"расм\": расм, \"услуб\": услуб,\n \"нависед\": нависед, \"ҳамчун_матн\": ҳамчун_матн, \"тоза_кун\": тоза_кун,\n \"қимат\": қимат,\n }\n return {name: Builtin(name, fn) for name, fn in names.items()}\n", "parser.py": "\"\"\"Парсер — tokens in, AST out.\n\nThis is a *recursive descent* parser: one small method per grammar rule, and\nthe methods call each other the same way the rules nest. It is the easiest kind\nof parser to read, to debug, and to extend.\n\nGrammar of version 0.9 (EBNF):\n\n program ::= { NEWLINE | statement } EOF\n\n statement ::= if_stmt | for_stmt | while_stmt | func_decl\n | return_stmt | import_stmt | declaration | expr_or_assign\n if_stmt ::= \"агар\" expression \":\" block\n [ \"вагарна\" ( \"агар\" ... | \":\" block ) ]\n for_stmt ::= \"барои\" IDENT \"аз\" expression [ \"то\" expression ] \":\" block\n while_stmt ::= \"то_вақте\" expression \":\" block\n func_decl ::= \"функсия\" IDENT \"(\" [ IDENT { \",\" IDENT } ] \")\" \":\" block\n return_stmt ::= \"баргардон\" [ expression ] NEWLINE\n import_stmt ::= \"ворид\" IDENT NEWLINE\n declaration ::= \"бигзор\" IDENT \"=\" expression NEWLINE\n expr_or_assign ::= expression [ \"=\" expression ] NEWLINE\n block ::= NEWLINE INDENT statement { statement } DEDENT\n\n expression ::= or_expr\n or_expr ::= and_expr { \"ё\" and_expr }\n and_expr ::= not_expr { \"ва\" not_expr }\n not_expr ::= \"не\" not_expr | comparison\n comparison ::= term [ (\"==\" | \"!=\" | \"<\" | \">\" | \"<=\" | \">=\") term ]\n term ::= factor { (\"+\" | \"-\") factor }\n factor ::= unary { (\"*\" | \"/\" | \"%\") unary }\n unary ::= \"-\" unary | postfix\n postfix ::= primary { \"(\" [ arguments ] \")\" | \"[\" expression \"]\"\n | \".\" IDENT }\n arguments ::= expression { \",\" expression }\n primary ::= NUMBER | STRING | IDENT | \"рост\" | \"дурӯғ\" | \"холӣ\"\n | list_literal | dict_literal | \"(\" expression \")\"\n list_literal ::= \"[\" [ expression { \",\" expression } [\",\"] ] \"]\"\n dict_literal ::= \"{\" [ pair { \",\" pair } [\",\"] ] \"}\"\n pair ::= expression \":\" expression\n\nPrecedence lives in the *shape* of those rules: `or_expr` is written in terms\nof `and_expr`, which is written in terms of `not_expr`, and so on down to\n`primary`. Each level down binds tighter, with no precedence table anywhere.\n\n`comparison` uses `[ ... ]` — at most one — rather than `{ ... }`. That makes\ncomparison non-associative, so `1 < 2 < 3` is a syntax error instead of the\nnonsense `(1 < 2) < 3`.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom . import ast_nodes as ast\nfrom .errors import ParseError\nfrom .tokens import COMPOUND_OPERATORS, KEYWORD_TOKENS, Token, TokenType\n\n# operator token -> the string stored on the AST node\nTERM_OPERATORS = {\n TokenType.PLUS: \"+\",\n TokenType.MINUS: \"-\",\n}\n\nFACTOR_OPERATORS = {\n TokenType.STAR: \"*\",\n TokenType.SLASH: \"/\",\n TokenType.PERCENT: \"%\",\n}\n\nCOMPARISON_OPERATORS = {\n TokenType.EQ: \"==\",\n TokenType.NEQ: \"!=\",\n TokenType.LT: \"<\",\n TokenType.GT: \">\",\n TokenType.LTE: \"<=\",\n TokenType.GTE: \">=\",\n}\n\n\nclass Parser:\n def __init__(self, tokens: list[Token], source_lines: list[str]) -> None:\n self.tokens = tokens\n self.source_lines = source_lines\n self.pos = 0\n self.function_depth = 0 # so `баргардон` outside a function is caught\n self.loop_depth = 0 # so `шикан` / `давом` outside a loop is caught\n\n # ------------------------------------------------------------------\n # low-level cursor helpers\n # ------------------------------------------------------------------\n def _current(self) -> Token:\n return self.tokens[self.pos]\n\n def _check(self, type_: TokenType) -> bool:\n return self._current().type is type_\n\n def _advance(self) -> Token:\n token = self.tokens[self.pos]\n if token.type is not TokenType.EOF:\n self.pos += 1\n return token\n\n def _match(self, type_: TokenType) -> bool:\n if self._check(type_):\n self._advance()\n return True\n return False\n\n def _source_line(self, line: int) -> str:\n if 1 <= line <= len(self.source_lines):\n return self.source_lines[line - 1]\n return \"\"\n\n def _error(self, token: Token, message: str, hint: str = \"\") -> ParseError:\n return ParseError(\n message, token.line, token.column, self._source_line(token.line), hint\n )\n\n def _expect(self, type_: TokenType, message: str, hint: str = \"\") -> Token:\n if self._check(type_):\n return self._advance()\n\n # A stray \"=\" where an expression was supposed to end means the student\n # wrote an assignment inside an expression — `навис(x = 1)`. Saying\n # \"closing bracket not found\" there would send them hunting for the\n # wrong thing, so that case gets its own message.\n #\n # This does not apply when we were expecting a *name* (`бигзор = 10`),\n # where the missing name is the real problem.\n if self._current().type is TokenType.ASSIGN and type_ in (\n TokenType.RPAREN,\n TokenType.RBRACKET,\n TokenType.COMMA,\n TokenType.NEWLINE,\n ):\n raise self._error(\n self._current(),\n 'Аломати \"=\" дар ин ҷо истифода намешавад.',\n hint=\"Додани қимат амали алоҳида аст: бигзор ном = қимат\",\n )\n\n raise self._error(self._current(), message, hint)\n\n def _end_statement(self) -> None:\n if self._check(TokenType.EOF):\n return\n self._expect(\n TokenType.NEWLINE,\n f'Пас аз ин амал сатри нав интизор мешуд, вале \"{self._current().value}\" ёфт шуд.',\n hint=\"Ҳар амал бояд дар сатри алоҳида навишта шавад.\",\n )\n\n # ------------------------------------------------------------------\n # grammar rules — statements\n # ------------------------------------------------------------------\n def parse(self) -> ast.Program:\n program = ast.Program(line=1, column=1, statements=[])\n\n while not self._check(TokenType.EOF):\n if self._match(TokenType.NEWLINE):\n continue # blank lines are not statements\n\n if self._check(TokenType.INDENT):\n raise self._error(\n self._current(),\n \"Ин сатр бе сабаб фосила дорад.\",\n hint='Фосила танҳо баъд аз \"агар\", \"барои\", \"функсия\" ва '\n \"монанди онҳо истифода мешавад.\",\n )\n\n program.statements.append(self._statement())\n\n return program\n\n def _statement(self) -> ast.Statement:\n if self._check(TokenType.IF):\n return self._if_statement()\n\n if self._check(TokenType.FOR):\n return self._for_statement()\n\n if self._check(TokenType.WHILE):\n return self._while_statement()\n\n if self._check(TokenType.FUNC):\n return self._function_declaration()\n\n if self._check(TokenType.RETURN):\n return self._return_statement()\n\n if self._check(TokenType.IMPORT):\n return self._import_statement()\n\n if self._check(TokenType.CLASS):\n return self._class_declaration()\n\n if self._check(TokenType.TRY):\n return self._try_statement()\n\n if self._check(TokenType.BREAK):\n return self._loop_jump(TokenType.BREAK, \"шикан\", ast.BreakStatement)\n\n if self._check(TokenType.CONTINUE):\n return self._loop_jump(TokenType.CONTINUE, \"давом\", ast.ContinueStatement)\n\n if self._check(TokenType.CATCH):\n raise self._error(\n self._current(),\n '\"хато\" бе \"кӯшиш\" истифода шудааст.',\n hint='Ҳар \"хато\" бояд пас аз блоки \"кӯшиш\" ояд.',\n )\n\n if self._check(TokenType.ELSE):\n raise self._error(\n self._current(),\n '\"вагарна\" бе \"агар\" истифода шудааст.',\n hint='Ҳар \"вагарна\" бояд ба як \"агар\" тааллуқ дошта бошад '\n \"ва дар ҳамон сатҳи фосила истад.\",\n )\n\n if self._check(TokenType.LET):\n return self._declaration()\n\n return self._expression_or_assignment()\n\n def _block(self, opener: Token) -> list[ast.Statement]:\n \"\"\"NEWLINE INDENT statement+ DEDENT — the body of a block statement.\n\n `opener` is the keyword that opened the block. An empty block is\n reported at *that* keyword rather than at the blank line after it,\n which may not exist at all when the file simply ends.\n \"\"\"\n self._expect(\n TokenType.NEWLINE,\n 'Пас аз \":\" бояд сатри нав сар шавад.',\n hint=f'Амалҳои \"{opener.value}\" -ро дар сатрҳои зерин нависед.',\n )\n\n # Blank and comment-only lines carry no indentation, so each one\n # arrives as a bare NEWLINE before the INDENT. Starting a block with\n # a comment explaining it is ordinary writing, not an error.\n while self._match(TokenType.NEWLINE):\n pass\n\n if not self._check(TokenType.INDENT):\n raise self._error(\n opener,\n f'Пас аз \"{opener.value}\" блоки холӣ мондааст.',\n hint=\"Сатрҳои дохили блок бояд 4 холигӣ фосила дошта бошанд.\",\n )\n self._advance()\n\n statements: list[ast.Statement] = []\n while not self._check(TokenType.DEDENT) and not self._check(TokenType.EOF):\n if self._match(TokenType.NEWLINE):\n continue\n statements.append(self._statement())\n\n self._expect(TokenType.DEDENT, \"Блок ба анҷом нарасид.\")\n return statements\n\n def _if_statement(self) -> ast.IfStatement:\n keyword = self._advance() # consume \"агар\"\n\n condition = self._expression()\n self._expect(\n TokenType.COLON,\n 'Пас аз шарти \"агар\" аломати \":\" лозим аст.',\n hint=\"Масалан: агар синну_сол >= 16:\",\n )\n then_branch = self._block(keyword)\n\n else_branch: list[ast.Statement] | None = None\n if self._check(TokenType.ELSE):\n else_keyword = self._advance() # consume \"вагарна\"\n\n if self._check(TokenType.IF):\n # `вагарна агар` — the whole chain is one nested IfStatement.\n else_branch = [self._if_statement()]\n else:\n self._expect(\n TokenType.COLON,\n 'Пас аз \"вагарна\" аломати \":\" лозим аст.',\n hint=\"Барои шарти дигар нависед: вагарна агар ... :\",\n )\n else_branch = self._block(else_keyword)\n\n return ast.IfStatement(\n line=keyword.line,\n column=keyword.column,\n condition=condition,\n then_branch=then_branch,\n else_branch=else_branch,\n )\n\n def _for_statement(self) -> ast.Statement:\n \"\"\"барои i аз 1 то 10: (a count)\n барои x аз рӯйхат: (a walk)\n\n Both start `барои НОМ аз ...`; the presence of `то` decides which. That\n is why walking a collection does not need a keyword of its own — `дар`\n stays available as an ordinary variable name.\n \"\"\"\n keyword = self._advance() # consume \"барои\"\n\n variable = self._expect(\n TokenType.IDENT,\n f'Пас аз \"барои\" номи тағйирёбанда лозим аст, вале '\n f'\"{self._current().value}\" ёфт шуд.',\n hint=\"Масалан: барои i аз 1 то 10:\",\n )\n\n self._expect(\n TokenType.FROM,\n 'Пас аз номи тағйирёбанда калимаи \"аз\" лозим аст.',\n hint=f\"Масалан: барои {variable.value} аз 1 то 10:\",\n )\n\n start = self._expression()\n\n if self._match(TokenType.TO):\n end = self._expression()\n self._expect(\n TokenType.COLON,\n 'Пас аз \"то <қимат>\" аломати \":\" лозим аст.',\n hint=f\"Масалан: барои {variable.value} аз 1 то 10:\",\n )\n return ast.ForRange(\n line=keyword.line,\n column=keyword.column,\n variable=variable.value,\n start=start,\n end=end,\n body=self._loop_block(keyword),\n )\n\n self._expect(\n TokenType.COLON,\n 'Пас аз \"барои ... аз ...\" аломати \":\" лозим аст.',\n hint=f\"Барои шумурдан: барои {variable.value} аз 1 то 10:\",\n )\n return ast.ForEach(\n line=keyword.line,\n column=keyword.column,\n variable=variable.value,\n iterable=start,\n body=self._loop_block(keyword),\n )\n\n def _while_statement(self) -> ast.WhileStatement:\n keyword = self._advance() # consume \"то_вақте\"\n\n condition = self._expression()\n self._expect(\n TokenType.COLON,\n 'Пас аз шарти \"то_вақте\" аломати \":\" лозим аст.',\n hint=\"Масалан: то_вақте ҳисоб < 10:\",\n )\n\n return ast.WhileStatement(\n line=keyword.line,\n column=keyword.column,\n condition=condition,\n body=self._loop_block(keyword),\n )\n\n def _loop_block(self, opener: Token) -> list[ast.Statement]:\n \"\"\"A block that `шикан` and `давом` are allowed inside.\"\"\"\n self.loop_depth += 1\n try:\n return self._block(opener)\n finally:\n self.loop_depth -= 1\n\n def _loop_jump(self, type_: TokenType, word: str, node_type):\n keyword = self._advance()\n\n # A function boundary stops the search: `шикан` inside a function\n # declared inside a loop does not belong to that loop.\n if self.loop_depth == 0:\n raise self._error(\n keyword,\n f'\"{word}\" танҳо дар дохили давр истифода мешавад.',\n hint='Онро дар дохили \"барои\" ё \"то_вақте\" нависед.',\n )\n\n self._end_statement()\n return node_type(line=keyword.line, column=keyword.column)\n\n def _try_statement(self) -> ast.TryStatement:\n keyword = self._advance() # consume \"кӯшиш\"\n self._expect(\n TokenType.COLON,\n 'Пас аз \"кӯшиш\" аломати \":\" лозим аст.',\n hint=\"Масалан: кӯшиш:\",\n )\n body = self._block(keyword)\n\n if not self._check(TokenType.CATCH):\n raise self._error(\n self._current(),\n 'Пас аз блоки \"кӯшиш\" блоки \"хато\" лозим аст.',\n hint=\"Масалан: кӯшиш: ... хато: ...\",\n )\n\n handlers: list[ast.CatchClause] = []\n seen_catch_all = False\n\n while self._check(TokenType.CATCH):\n catch = self._advance()\n\n # `хато \"тақсим_ба_сифр\" паём:` — the text names the kind of\n # failure, the name binds its message. Both are optional.\n kind: str | None = None\n if self._check(TokenType.STRING):\n kind = self._advance().value\n\n name: str | None = None\n if self._check(TokenType.IDENT):\n name = self._advance().value\n\n if seen_catch_all:\n raise self._error(\n catch,\n 'Пас аз \"хато\" -и умумӣ блоки дигар ҳеҷ гоҳ иҷро намешавад.',\n hint='Блоки бе навъро охирин гузоред.',\n )\n if kind is None:\n seen_catch_all = True\n\n self._expect(\n TokenType.COLON,\n 'Пас аз \"хато\" аломати \":\" лозим аст.',\n hint=\"Барои гирифтани матни хато: хато паём:\",\n )\n handlers.append(\n ast.CatchClause(\n line=catch.line,\n column=catch.column,\n kind=kind,\n name=name,\n body=self._block(catch),\n )\n )\n\n return ast.TryStatement(\n line=keyword.line,\n column=keyword.column,\n body=body,\n handlers=handlers,\n )\n\n def _class_declaration(self) -> ast.ClassDeclaration:\n keyword = self._advance() # consume \"синф\"\n\n name = self._expect(\n TokenType.IDENT,\n f'Пас аз \"қолиб\" ном лозим аст, вале \"{self._current().value}\" ёфт шуд.',\n hint=\"Масалан: қолиб Донишҷӯ:\",\n )\n\n parent: str | None = None\n if self._match(TokenType.INHERITS):\n parent = self._expect(\n TokenType.IDENT,\n 'Пас аз \"мерос\" номи синфи волид лозим аст.',\n hint=\"Масалан: қолиб Аълочӣ мерос Донишҷӯ:\",\n ).value\n\n self._expect(\n TokenType.COLON,\n 'Пас аз номи синф аломати \":\" лозим аст.',\n hint=f\"Масалан: қолиб {name.value}:\",\n )\n\n # A class body holds only functions, so say that plainly rather than\n # letting a stray statement fail somewhere stranger.\n self._expect(\n TokenType.NEWLINE, 'Пас аз \":\" бояд сатри нав сар шавад.'\n )\n while self._match(TokenType.NEWLINE):\n pass\n if not self._check(TokenType.INDENT):\n raise self._error(\n keyword,\n f'Синфи \"{name.value}\" холӣ мондааст.',\n hint=\"Синф бояд ҳадди аққал як функсия дошта бошад.\",\n )\n self._advance()\n\n methods: list[ast.FunctionDeclaration] = []\n while not self._check(TokenType.DEDENT) and not self._check(TokenType.EOF):\n if self._match(TokenType.NEWLINE):\n continue\n if not self._check(TokenType.FUNC):\n raise self._error(\n self._current(),\n \"Дар дохили синф танҳо функсияҳо навишта мешаванд.\",\n hint=\"Қиматҳоро дар функсияи «оғоз» муайян кунед.\",\n )\n methods.append(self._function_declaration())\n\n self._expect(TokenType.DEDENT, \"Блоки синф ба анҷом нарасид.\")\n return ast.ClassDeclaration(\n line=keyword.line,\n column=keyword.column,\n name=name.value,\n parent=parent,\n methods=methods,\n )\n\n def _function_declaration(self) -> ast.FunctionDeclaration:\n keyword = self._advance() # consume \"функсия\"\n\n name = self._expect(\n TokenType.IDENT,\n f'Пас аз \"функсия\" ном лозим аст, вале \"{self._current().value}\" ёфт шуд.',\n hint=\"Масалан: функсия ҷамъ(a, b):\",\n )\n\n self._expect(\n TokenType.LPAREN,\n f'Пас аз номи функсия қавси \"(\" лозим аст.',\n hint=f\"Масалан: функсия {name.value}(a, b):\",\n )\n\n parameters: list[str] = []\n if not self._check(TokenType.RPAREN):\n while True:\n parameter = self._expect(\n TokenType.IDENT,\n \"Номи параметр интизор мешуд, вале \"\n f'\"{self._current().value}\" ёфт шуд.',\n )\n if parameter.value in parameters:\n raise self._error(\n parameter,\n f'Параметри \"{parameter.value}\" ду бор навишта шудааст.',\n hint=\"Ҳар параметр бояд номи ягона дошта бошад.\",\n )\n parameters.append(parameter.value)\n if not self._match(TokenType.COMMA):\n break\n\n self._expect(TokenType.RPAREN, 'Қавси пӯшида \")\" ёфт нашуд.')\n self._expect(\n TokenType.COLON,\n 'Пас аз рӯйхати параметрҳо аломати \":\" лозим аст.',\n hint=f\"Масалан: функсия {name.value}(...):\",\n )\n\n self.function_depth += 1\n enclosing_loops, self.loop_depth = self.loop_depth, 0\n try:\n body = self._block(keyword)\n finally:\n self.function_depth -= 1\n self.loop_depth = enclosing_loops\n\n return ast.FunctionDeclaration(\n line=keyword.line,\n column=keyword.column,\n name=name.value,\n parameters=parameters,\n body=body,\n )\n\n def _return_statement(self) -> ast.ReturnStatement:\n keyword = self._advance() # consume \"баргардон\"\n\n if self.function_depth == 0:\n raise self._error(\n keyword,\n '\"баргардон\" танҳо дар дохили функсия истифода мешавад.',\n hint=\"Берун аз функсия чизе барои баргардонидан нест.\",\n )\n\n value: ast.Expression | None = None\n if not self._check(TokenType.NEWLINE) and not self._check(TokenType.EOF):\n value = self._expression()\n\n self._end_statement()\n return ast.ReturnStatement(\n line=keyword.line, column=keyword.column, value=value\n )\n\n def _import_statement(self) -> ast.ImportStatement:\n keyword = self._advance() # consume \"ворид\"\n\n name = self._expect(\n TokenType.IDENT,\n f'Пас аз \"ворид\" номи модул лозим аст, вале '\n f'\"{self._current().value}\" ёфт шуд.',\n hint=\"Масалан: ворид риёзӣ\",\n )\n self._end_statement()\n\n return ast.ImportStatement(\n line=keyword.line, column=keyword.column, name=name.value\n )\n\n def _declaration(self) -> ast.VarDeclaration:\n keyword = self._advance() # consume \"бигзор\"\n\n name = self._expect(\n TokenType.IDENT,\n f'Пас аз \"бигзор\" номи тағйирёбанда лозим аст, вале '\n f'\"{self._current().value}\" ёфт шуд.',\n hint=\"Масалан: бигзор синну_сол = 16\",\n )\n\n self._expect(\n TokenType.ASSIGN,\n 'Пас аз номи тағйирёбанда аломати \"=\" лозим аст.',\n hint=f\"Масалан: бигзор {name.value} = 0\",\n )\n\n value = self._expression()\n self._end_statement()\n\n return ast.VarDeclaration(\n line=keyword.line, column=keyword.column, name=name.value, value=value\n )\n\n def _expression_or_assignment(self) -> ast.Statement:\n \"\"\"One rule for `навис(x)`, `x = 1` and `рӯйхат[0] = 1`.\n\n Parse an expression first, then look for `=`. Deciding afterwards is\n simpler than looking ahead, and it extends to any assignable form\n without touching the statement dispatcher.\n \"\"\"\n token = self._current()\n expression = self._expression()\n\n compound = COMPOUND_OPERATORS.get(self._current().type)\n\n if self._check(TokenType.ASSIGN) or compound:\n operator = self._advance()\n value = self._expression()\n self._end_statement()\n\n # `x += 1` becomes `x = x + 1` right here, so nothing downstream\n # ever learns that compound assignment exists.\n if compound:\n value = ast.BinaryOp(\n line=operator.line,\n column=operator.column,\n operator=compound,\n left=expression,\n right=value,\n )\n\n if isinstance(expression, ast.Identifier):\n return ast.Assignment(\n line=token.line,\n column=token.column,\n name=expression.name,\n value=value,\n )\n\n if isinstance(expression, ast.IndexExpression):\n return ast.IndexAssignment(\n line=token.line,\n column=token.column,\n target=expression.target,\n index=expression.index,\n value=value,\n )\n\n if isinstance(expression, ast.MemberExpression):\n return ast.MemberAssignment(\n line=token.line,\n column=token.column,\n target=expression.target,\n name=expression.name,\n value=value,\n )\n\n raise self._error(\n token,\n \"Ба ин ифода қимат додан мумкин нест.\",\n hint=\"Дар тарафи чапи «=» бояд ном ё унсури рӯйхат бошад.\",\n )\n\n self._end_statement()\n return ast.ExpressionStatement(\n line=token.line, column=token.column, expression=expression\n )\n\n # ------------------------------------------------------------------\n # grammar rules — expressions\n # ------------------------------------------------------------------\n def _expression(self) -> ast.Expression:\n return self._pipeline()\n\n def _pipeline(self) -> ast.Expression:\n \"\"\"`номҳо |> тартиб |> навис` — left to right, in Tajik word order.\n\n Tajik puts the verb last: object, then what is done to it. Every\n programming language puts the verb first. This rule lets a student\n write either, and it is pure rewriting — `а |> f` becomes `f(а)`, and\n `а |> f(б)` becomes `f(а, б)`, so nothing downstream learns about it.\n \"\"\"\n node = self._or()\n\n while self._check(TokenType.PIPE):\n pipe = self._advance()\n right = self._or()\n\n if isinstance(right, ast.CallExpression):\n right.arguments.insert(0, node)\n node = right\n else:\n node = ast.CallExpression(\n line=pipe.line,\n column=pipe.column,\n callee=right,\n arguments=[node],\n )\n\n return node\n\n def _or(self) -> ast.Expression:\n node = self._and()\n\n while self._check(TokenType.OR):\n operator = self._advance()\n right = self._and()\n node = ast.LogicalOp(\n line=operator.line,\n column=operator.column,\n operator=\"ё\",\n left=node,\n right=right,\n )\n\n return node\n\n def _and(self) -> ast.Expression:\n node = self._not()\n\n while self._check(TokenType.AND):\n operator = self._advance()\n right = self._not()\n node = ast.LogicalOp(\n line=operator.line,\n column=operator.column,\n operator=\"ва\",\n left=node,\n right=right,\n )\n\n return node\n\n def _not(self) -> ast.Expression:\n if self._check(TokenType.NOT):\n operator = self._advance()\n return ast.UnaryOp(\n line=operator.line,\n column=operator.column,\n operator=\"не\",\n operand=self._not(), # so that `не не x` parses\n )\n\n return self._comparison()\n\n def _comparison(self) -> ast.Expression:\n node = self._term()\n\n if self._current().type in COMPARISON_OPERATORS:\n operator = self._advance()\n right = self._term()\n node = ast.BinaryOp(\n line=operator.line,\n column=operator.column,\n operator=COMPARISON_OPERATORS[operator.type],\n left=node,\n right=right,\n )\n\n # Deliberately not a loop: `1 < 2 < 3` reads like mathematics but\n # would mean `(1 < 2) < 3` — comparing a boolean with a number.\n # Rejecting it here is clearer than letting it fail at runtime.\n if self._current().type in COMPARISON_OPERATORS:\n second = self._current()\n raise self._error(\n second,\n \"Ду муқоиса паси ҳам навишта намешаванд.\",\n hint='Онҳоро бо \"ва\" ҷудо кунед: 1 < 2 ва 2 < 3',\n )\n\n return node\n\n def _term(self) -> ast.Expression:\n node = self._factor()\n\n while self._current().type in TERM_OPERATORS:\n operator = self._advance()\n right = self._factor()\n node = ast.BinaryOp(\n line=operator.line,\n column=operator.column,\n operator=TERM_OPERATORS[operator.type],\n left=node,\n right=right,\n )\n\n return node\n\n def _factor(self) -> ast.Expression:\n node = self._unary()\n\n while self._current().type in FACTOR_OPERATORS:\n operator = self._advance()\n right = self._unary()\n node = ast.BinaryOp(\n line=operator.line,\n column=operator.column,\n operator=FACTOR_OPERATORS[operator.type],\n left=node,\n right=right,\n )\n\n return node\n\n def _unary(self) -> ast.Expression:\n if self._check(TokenType.MINUS):\n operator = self._advance()\n operand = self._unary() # so that --x parses\n return ast.UnaryOp(\n line=operator.line,\n column=operator.column,\n operator=\"-\",\n operand=operand,\n )\n\n return self._postfix()\n\n def _postfix(self) -> ast.Expression:\n \"\"\"Calls, indexing and member access, in any order and any number.\n\n `модул.функсия(рӯйхат[0])` is one loop, not three special cases.\n \"\"\"\n expression = self._primary()\n\n while True:\n if self._check(TokenType.LPAREN):\n expression = self._finish_call(expression)\n elif self._check(TokenType.LBRACKET):\n bracket = self._advance()\n index = self._expression()\n self._expect(\n TokenType.RBRACKET,\n 'Қавси пӯшида \"]\" ёфт нашуд.',\n hint='Ҳар қавси \"[\" бояд бо \"]\" пӯшида шавад.',\n )\n expression = ast.IndexExpression(\n line=bracket.line,\n column=bracket.column,\n target=expression,\n index=index,\n )\n elif self._check(TokenType.DOT):\n dot = self._advance()\n\n # After a dot, a keyword is just a name. `питон.ворид(...)`\n # must work even though `ворид` opens an import statement —\n # a member name is never ambiguous with a statement here.\n current = self._current()\n if current.type is TokenType.IDENT or (\n isinstance(current.value, str)\n and current.value in KEYWORD_TOKENS\n ):\n name = self._advance()\n else:\n raise self._error(\n current,\n 'Пас аз \".\" ном лозим аст.',\n hint=\"Масалан: риёзӣ.реша(9)\",\n )\n\n expression = ast.MemberExpression(\n line=dot.line,\n column=dot.column,\n target=expression,\n name=name.value,\n )\n else:\n return expression\n\n def _finish_call(self, callee: ast.Expression) -> ast.CallExpression:\n paren = self._advance() # consume \"(\"\n arguments: list[ast.Expression] = []\n\n if not self._check(TokenType.RPAREN):\n arguments.append(self._expression())\n while self._match(TokenType.COMMA):\n # A trailing comma is allowed, as in list and dictionary\n # literals. Multi-line calls are where it earns its keep:\n # adding an argument stays a one-line change.\n if self._check(TokenType.RPAREN):\n break\n arguments.append(self._expression())\n\n self._expect(\n TokenType.RPAREN,\n 'Қавси пӯшида \")\" ёфт нашуд.',\n hint='Ҳар қавси \"(\" бояд бо \")\" пӯшида шавад.',\n )\n\n return ast.CallExpression(\n line=paren.line, column=paren.column, callee=callee, arguments=arguments\n )\n\n def _list_literal(self) -> ast.ListLiteral:\n bracket = self._advance() # consume \"[\"\n elements: list[ast.Expression] = []\n\n while not self._check(TokenType.RBRACKET):\n elements.append(self._expression())\n if not self._match(TokenType.COMMA):\n break\n\n self._expect(\n TokenType.RBRACKET,\n 'Қавси пӯшида \"]\" ёфт нашуд.',\n hint=\"Масалан: [1, 2, 3]\",\n )\n return ast.ListLiteral(\n line=bracket.line, column=bracket.column, elements=elements\n )\n\n def _dict_literal(self) -> ast.DictLiteral:\n brace = self._advance() # consume \"{\"\n pairs: list[tuple[ast.Expression, ast.Expression]] = []\n\n while not self._check(TokenType.RBRACE):\n key = self._expression()\n self._expect(\n TokenType.COLON,\n 'Дар луғат пас аз калид аломати \":\" лозим аст.',\n hint='Масалан: {\"ном\": \"Баҳром\"}',\n )\n pairs.append((key, self._expression()))\n if not self._match(TokenType.COMMA):\n break\n\n self._expect(\n TokenType.RBRACE,\n 'Қавси пӯшида \"}\" ёфт нашуд.',\n hint='Масалан: {\"ном\": \"Баҳром\"}',\n )\n return ast.DictLiteral(line=brace.line, column=brace.column, pairs=pairs)\n\n def _primary(self) -> ast.Expression:\n token = self._current()\n\n if self._check(TokenType.NUMBER):\n self._advance()\n return ast.NumberLiteral(token.line, token.column, token.value)\n\n if self._check(TokenType.STRING):\n self._advance()\n return ast.StringLiteral(token.line, token.column, token.value)\n\n if self._check(TokenType.TRUE):\n self._advance()\n return ast.BooleanLiteral(token.line, token.column, True)\n\n if self._check(TokenType.FALSE):\n self._advance()\n return ast.BooleanLiteral(token.line, token.column, False)\n\n if self._check(TokenType.NULL):\n self._advance()\n return ast.NullLiteral(token.line, token.column)\n\n if self._check(TokenType.LBRACKET):\n return self._list_literal()\n\n if self._check(TokenType.LBRACE):\n return self._dict_literal()\n\n if self._check(TokenType.IDENT):\n self._advance()\n return ast.Identifier(token.line, token.column, token.value)\n\n if self._match(TokenType.LPAREN):\n inner = self._expression()\n self._expect(TokenType.RPAREN, 'Қавси пӯшида \")\" ёфт нашуд.')\n return inner\n\n if self._check(TokenType.ASSIGN):\n raise self._error(\n token,\n 'Аломати \"=\" дар ин ҷо истифода намешавад.',\n hint=\"Барои муайян кардани тағйирёбанда: бигзор ном = қимат\",\n )\n\n if self._check(TokenType.NEWLINE) or self._check(TokenType.EOF):\n raise self._error(\n token,\n \"Дар ин ҷо қимат интизор мешуд, вале сатр тамом шуд.\",\n hint=\"Матн, рақам ё номи тағйирёбандаро илова кунед.\",\n )\n\n raise self._error(\n token,\n f'Дар ин ҷо қимат интизор мешуд, вале \"{token.value}\" ёфт шуд.',\n hint=\"Қимат метавонад матн, рақам ё ном бошад.\",\n )\n", "stdlib.py": "\"\"\"Китобхонаи стандартӣ — the built-in names and modules.\n\nEverything here is available **without `ворид`**, the way Python's builtins\nare. The two modules (`риёзӣ`, `матн`) exist as tidy namespaces over the very\nsame function objects, so `гирд(3.7)` and `риёзӣ.гирд(3.7)` are the same call.\n\nA builtin reports problems by raising `BuiltinError`; the interpreter turns\nthat into a normal Tajik runtime error with the caller's line and column.\n\nNaming rules followed here, so the library stays predictable:\n\n* a function that *answers a question* is a noun or adjective — `дарозӣ`,\n `навъ`, `калидҳо`;\n* a function that *does something* is an imperative — `илова`, `иваз`,\n `хонед`;\n* a function that *asks* yes/no ends in a verb — `дорад`, `сар_мешавад`.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport math\nimport random\nfrom pathlib import Path\nfrom typing import Any, Callable\n\nfrom . import collation\nfrom .errors import BuiltinError\nfrom .values import Builtin, is_number, to_tajik_string, type_name\n\n\n# ---------------------------------------------------------------------------\n# argument checking helpers\n# ---------------------------------------------------------------------------\ndef _arity(name: str, args: tuple[Any, ...], *allowed: int) -> None:\n if len(args) not in allowed:\n wanted = \" ё \".join(str(n) for n in allowed)\n raise BuiltinError(\n f'Функсияи \"{name}\" {wanted} аргумент мехоҳад, вале '\n f\"{len(args)} гирифт.\",\n hint=\"Шумораи аргументҳоро санҷед.\",\n )\n\n\ndef _need(name: str, value: Any, check: Callable[[Any], bool], expected: str) -> Any:\n if not check(value):\n raise BuiltinError(\n f'Функсияи \"{name}\" {expected} мехоҳад, вале {type_name(value)} гирифт.'\n )\n return value\n\n\ndef _text(name: str, value: Any) -> str:\n return _need(name, value, lambda v: isinstance(v, str), \"матн\")\n\n\ndef _list(name: str, value: Any) -> list:\n return _need(name, value, lambda v: isinstance(v, list), \"рӯйхат\")\n\n\ndef _dict(name: str, value: Any) -> dict:\n return _need(name, value, lambda v: isinstance(v, dict), \"луғат\")\n\n\ndef _number(name: str, value: Any) -> Any:\n return _need(name, value, is_number, \"рақам\")\n\n\ndef _whole(name: str, value: Any) -> int:\n if not is_number(value) or (isinstance(value, float) and not value.is_integer()):\n raise BuiltinError(\n f'Функсияи \"{name}\" рақами бутун мехоҳад, вале '\n f\"{type_name(value)} гирифт.\"\n )\n return int(value)\n\n\ndef _position(name: str, index: Any, length: int, allow_end: bool = False) -> int:\n \"\"\"A 0-based index, accepting negatives from the end, as `рӯйхат[-1]` does.\"\"\"\n position = _whole(name, index)\n if position < 0:\n position += length\n limit = length if allow_end else length - 1\n if position < 0 or position > limit:\n raise BuiltinError(\n f\"Рақами тартибӣ берун аз ҳудуд аст (дарозӣ {length}).\",\n hint=(\n f\"Рақамҳои имконпазир: 0 то {limit}, ё -1 то -{length} аз охир.\"\n if length\n else \"Ин холӣ аст.\"\n ),\n )\n return position\n\n\ndef _ordered(name: str, items: list) -> list:\n \"\"\"Sorted copy — text by the Tajik alphabet, numbers numerically.\"\"\"\n if all(isinstance(item, str) for item in items):\n return sorted(items, key=collation.sort_key)\n if all(is_number(item) for item in items):\n return sorted(items)\n raise BuiltinError(\n f'Функсияи \"{name}\" рӯйхати ҳамаи матнҳо ё ҳамаи рақамҳоро мехоҳад.',\n hint='Барои рӯйхати мураккаб \"тартиб_бо\" -ро истифода баред.',\n )\n\n\n# ---------------------------------------------------------------------------\n# numbers — also exposed as the `риёзӣ` module\n# ---------------------------------------------------------------------------\ndef _build_math() -> dict[str, Any]:\n def реша(*args: Any) -> float:\n _arity(\"реша\", args, 1)\n value = _number(\"реша\", args[0])\n if value < 0:\n raise BuiltinError(\n \"Решаи квадратии рақами манфӣ вуҷуд надорад.\",\n hint=\"Аввал санҷед: агар x >= 0:\",\n )\n return math.sqrt(value)\n\n def дараҷа(*args: Any) -> Any:\n _arity(\"дараҷа\", args, 2)\n return _number(\"дараҷа\", args[0]) ** _number(\"дараҷа\", args[1])\n\n def мутлақ(*args: Any) -> Any:\n _arity(\"мутлақ\", args, 1)\n return abs(_number(\"мутлақ\", args[0]))\n\n def гирд(*args: Any) -> Any:\n \"\"\"Гирд мекунад. Бо аргументи дуюм — то чанд рақами баъди вергул.\"\"\"\n _arity(\"гирд\", args, 1, 2)\n value = _number(\"гирд\", args[0])\n if len(args) == 1:\n return round(value)\n return round(value, _whole(\"гирд\", args[1]))\n\n def фарш(*args: Any) -> int:\n _arity(\"фарш\", args, 1)\n return math.floor(_number(\"фарш\", args[0]))\n\n def сақф(*args: Any) -> int:\n _arity(\"сақф\", args, 1)\n return math.ceil(_number(\"сақф\", args[0]))\n\n def бутун(*args: Any) -> int:\n \"\"\"Қисми касриро мепартояд: бутун(3.9) = 3, бутун(-3.9) = -3.\"\"\"\n _arity(\"бутун\", args, 1)\n value = args[0]\n if isinstance(value, str):\n return int(ба_рақам_text(\"бутун\", value))\n return int(_number(\"бутун\", value))\n\n def мин(*args: Any) -> Any:\n return _extreme_of(\"мин\", args, biggest=False)\n\n def макс(*args: Any) -> Any:\n return _extreme_of(\"макс\", args, biggest=True)\n\n def _extreme_of(name: str, args: tuple[Any, ...], biggest: bool) -> Any:\n \"\"\"мин(a, b) or мин(рӯйхат) — both read naturally, so both work.\"\"\"\n if len(args) == 1 and isinstance(args[0], list):\n items = args[0]\n if not items:\n raise BuiltinError(\n f'Функсияи \"{name}\" рӯйхати холиро қабул намекунад.',\n hint=\"Аввал санҷед: агар дарозӣ(рӯйхат) > 0:\",\n )\n elif len(args) == 2:\n items = list(args)\n else:\n raise BuiltinError(\n f'Функсияи \"{name}\" ду рақам ё як рӯйхат мехоҳад.',\n hint=f\"{name}(3, 7) ё {name}([3, 7, 1])\",\n )\n ordered = _ordered(name, items)\n return ordered[-1] if biggest else ordered[0]\n\n def тасодуфӣ(*args: Any) -> int:\n _arity(\"тасодуфӣ\", args, 2)\n low = _whole(\"тасодуфӣ\", args[0])\n high = _whole(\"тасодуфӣ\", args[1])\n if low > high:\n raise BuiltinError(\n \"Ҳадди поён аз ҳадди боло калон аст.\",\n hint=\"тасодуфӣ(1, 10) — аввал хурдтарин.\",\n )\n return random.randint(low, high)\n\n def интихоб(*args: Any) -> Any:\n \"\"\"Як унсури тасодуфӣ аз рӯйхат.\"\"\"\n _arity(\"интихоб\", args, 1)\n items = _list(\"интихоб\", args[0])\n if not items:\n raise BuiltinError('Функсияи \"интихоб\" рӯйхати холиро қабул намекунад.')\n return random.choice(items)\n\n values: dict[str, Any] = {\n name: Builtin(name, fn)\n for name, fn in {\n \"реша\": реша, \"дараҷа\": дараҷа, \"мутлақ\": мутлақ, \"гирд\": гирд,\n \"фарш\": фарш, \"сақф\": сақф, \"бутун\": бутун, \"мин\": мин,\n \"макс\": макс, \"тасодуфӣ\": тасодуфӣ, \"интихоб\": интихоб,\n }.items()\n }\n values[\"ПИ\"] = math.pi\n values[\"Е\"] = math.e\n return values\n\n\ndef ба_рақам_text(name: str, value: str) -> Any:\n \"\"\"Shared by `ба_рақам` and `бутун` so both fail the same way.\"\"\"\n text = value.strip()\n try:\n return int(text) if text.lstrip(\"-\").isdigit() else float(text)\n except ValueError:\n raise BuiltinError(\n f'Матни \"{value}\" ба рақам табдил намеёбад.',\n hint='Танҳо матни рақамӣ, масалан \"16\". Бо \"рақам_аст\" санҷед.',\n ) from None\n\n\ndef _буриш(*args: Any) -> Any:\n \"\"\"Пораи матн ё рӯйхат — ҳарду ҳадд дохил мешаванд, мисли «то».\n\n Module-level so that it can be both a global and `матн.буриш` without\n two copies drifting apart.\n \"\"\"\n _arity(\"буриш\", args, 3)\n target = args[0]\n if not isinstance(target, (str, list)):\n raise BuiltinError(\n f'Функсияи \"буриш\" матн ё рӯйхат мехоҳад, вале '\n f\"{type_name(target)} гирифт.\"\n )\n if not target:\n raise BuiltinError('Функсияи \"буриш\" чизи холиро қабул намекунад.')\n start = _position(\"буриш\", args[1], len(target))\n end = _position(\"буриш\", args[2], len(target))\n if end < start:\n raise BuiltinError(\n \"Ҳадди охир аз ҳадди аввал хурд аст.\",\n hint=\"буриш(x, 0, 2) — аввал хурдтарин.\",\n )\n return target[start : end + 1]\n\n\n# ---------------------------------------------------------------------------\n# text — also exposed as the `матн` module\n# ---------------------------------------------------------------------------\ndef _build_text() -> dict[str, Any]:\n def калон(*args: Any) -> str:\n _arity(\"калон\", args, 1)\n return _text(\"калон\", args[0]).upper()\n\n def хурд(*args: Any) -> str:\n _arity(\"хурд\", args, 1)\n return _text(\"хурд\", args[0]).lower()\n\n def тоза(*args: Any) -> str:\n \"\"\"Холигиҳои аввал ва охирро мебарорад.\"\"\"\n _arity(\"тоза\", args, 1)\n return _text(\"тоза\", args[0]).strip()\n\n def ҷудо(*args: Any) -> list:\n \"\"\"ҷудо(\"а,б,в\", \",\") -> [\"а\", \"б\", \"в\"]\"\"\"\n _arity(\"ҷудо\", args, 1, 2)\n text = _text(\"ҷудо\", args[0])\n if len(args) == 1:\n return text.split()\n separator = _text(\"ҷудо\", args[1])\n if separator == \"\":\n raise BuiltinError(\n \"Ҷудокунанда набояд холӣ бошад.\",\n hint=\"Барои ҳарф ба ҳарф: ҳарфҳо(матн)\",\n )\n return text.split(separator)\n\n def пайваст(*args: Any) -> str:\n \"\"\"пайваст([\"а\", \"б\"], \"-\") -> \"а-б\" \"\"\"\n _arity(\"пайваст\", args, 1, 2)\n items = _list(\"пайваст\", args[0])\n separator = _text(\"пайваст\", args[1]) if len(args) == 2 else \"\"\n for item in items:\n if not isinstance(item, str):\n raise BuiltinError(\n f'Функсияи \"пайваст\" рӯйхати матнҳоро мехоҳад, вале дар он '\n f\"{type_name(item)} ҳаст.\",\n hint=\"Ҳар қиматро бо «ба_матн» табдил диҳед.\",\n )\n return separator.join(items)\n\n def иваз(*args: Any) -> str:\n \"\"\"иваз(матн, кӯҳна, нав)\"\"\"\n _arity(\"иваз\", args, 3)\n text = _text(\"иваз\", args[0])\n old = _text(\"иваз\", args[1])\n new = _text(\"иваз\", args[2])\n if old == \"\":\n raise BuiltinError(\"Матни ивазшаванда набояд холӣ бошад.\")\n return text.replace(old, new)\n\n def ёфтан(*args: Any) -> Any:\n \"\"\"Рақами тартибии аввалин мувофиқат, ё «холӣ» агар ёфт нашавад.\"\"\"\n _arity(\"ёфтан\", args, 2)\n text = _text(\"ёфтан\", args[0])\n position = text.find(_text(\"ёфтан\", args[1]))\n return None if position < 0 else position\n\n def сар_мешавад(*args: Any) -> bool:\n _arity(\"сар_мешавад\", args, 2)\n return _text(\"сар_мешавад\", args[0]).startswith(\n _text(\"сар_мешавад\", args[1])\n )\n\n def тамом_мешавад(*args: Any) -> bool:\n _arity(\"тамом_мешавад\", args, 2)\n return _text(\"тамом_мешавад\", args[0]).endswith(\n _text(\"тамом_мешавад\", args[1])\n )\n\n def такрор(*args: Any) -> str:\n _arity(\"такрор\", args, 2)\n text = _text(\"такрор\", args[0])\n times = _whole(\"такрор\", args[1])\n if times < 0:\n raise BuiltinError(\"Шумораи такрор набояд манфӣ бошад.\")\n return text * times\n\n def ҳарфҳо(*args: Any) -> list:\n _arity(\"ҳарфҳо\", args, 1)\n return list(_text(\"ҳарфҳо\", args[0]))\n\n def рақам_аст(*args: Any) -> bool:\n \"\"\"Оё ин матнро ба рақам табдил додан мумкин аст?\"\"\"\n _arity(\"рақам_аст\", args, 1)\n text = _text(\"рақам_аст\", args[0]).strip()\n if text.lstrip(\"-\").isdigit():\n return True\n try:\n float(text)\n return True\n except ValueError:\n return False\n\n return {\n name: Builtin(name, fn)\n for name, fn in {\n \"калон\": калон, \"хурд\": хурд, \"тоза\": тоза, \"ҷудо\": ҷудо,\n \"буриш\": _буриш,\n \"пайваст\": пайваст, \"иваз\": иваз, \"ёфтан\": ёфтан,\n \"сар_мешавад\": сар_мешавад, \"тамом_мешавад\": тамом_мешавад,\n \"такрор\": такрор, \"ҳарфҳо\": ҳарфҳо, \"рақам_аст\": рақам_аст,\n }.items()\n }\n\n\n# ---------------------------------------------------------------------------\n# global builtins\n# ---------------------------------------------------------------------------\ndef build_globals(\n output: Callable[[str], None],\n read_line: Callable[[str], str | None] | None = None,\n call: Callable[[Any, list[Any]], Any] | None = None,\n) -> dict[str, Builtin]:\n \"\"\"Every name a program starts with.\n\n `read_line` is how `хонед` asks for input — the terminal passes Python's\n `input`, the browser passes an input pane, a test passes canned lines.\n\n `call` is how a builtin calls back into a TajikLang function, which is\n what `тартиб_бо` needs. The interpreter supplies it; nothing else can.\n \"\"\"\n\n # --- output and input ---------------------------------------------------\n def навис(*args: Any) -> None:\n output(\" \".join(to_tajik_string(a) for a in args))\n\n def хонед(*args: Any) -> str:\n \"\"\"хонед() ё хонед(\"Номи шумо: \") — always returns матн.\"\"\"\n _arity(\"хонед\", args, 0, 1)\n prompt = to_tajik_string(args[0]) if args else \"\"\n\n if read_line is None:\n raise BuiltinError(\n \"Дар ин ҷо маълумот хондан имконнопазир аст.\",\n hint=\"Функсияи «хонед» дар терминал ва майдончаи браузер кор мекунад.\",\n )\n\n line = read_line(prompt)\n if line is None:\n raise BuiltinError(\n \"Маълумоти воридотӣ тамом шуд.\",\n hint=\"Барнома чизе интизор буд, вале ҳеҷ чиз ворид нашуд.\",\n )\n return line\n\n # --- conversions and types ---------------------------------------------\n def ба_рақам(*args: Any) -> Any:\n _arity(\"ба_рақам\", args, 1)\n value = args[0]\n if is_number(value):\n return value\n if isinstance(value, str):\n return ба_рақам_text(\"ба_рақам\", value)\n raise BuiltinError(\n f'Функсияи \"ба_рақам\" матн мехоҳад, вале {type_name(value)} гирифт.'\n )\n\n def ба_матн(*args: Any) -> str:\n _arity(\"ба_матн\", args, 1)\n return to_tajik_string(args[0])\n\n def навъ(*args: Any) -> str:\n _arity(\"навъ\", args, 1)\n return type_name(args[0])\n\n # --- size, membership, copying -----------------------------------------\n def дарозӣ(*args: Any) -> int:\n _arity(\"дарозӣ\", args, 1)\n value = args[0]\n if isinstance(value, (str, list, dict)):\n return len(value)\n raise BuiltinError(\n f'Функсияи \"дарозӣ\" матн, рӯйхат ё луғат мехоҳад, вале '\n f\"{type_name(value)} гирифт.\"\n )\n\n def дорад(*args: Any) -> bool:\n _arity(\"дорад\", args, 2)\n container = args[0]\n if isinstance(container, (dict, list)):\n return args[1] in container\n if isinstance(container, str):\n return isinstance(args[1], str) and args[1] in container\n raise BuiltinError(\n f'Функсияи \"дорад\" луғат, рӯйхат ё матн мехоҳад, вале '\n f\"{type_name(container)} гирифт.\"\n )\n\n def нусха(*args: Any) -> Any:\n \"\"\"Нусхаи мустақил. Бе он «бигзор б = а» ҳамон рӯйхатро нишон медиҳад.\"\"\"\n _arity(\"нусха\", args, 1)\n value = args[0]\n if isinstance(value, list):\n return list(value)\n if isinstance(value, dict):\n return dict(value)\n return value # numbers, text and booleans cannot be changed anyway\n\n def холӣ_аст(*args: Any) -> bool:\n _arity(\"холӣ_аст\", args, 1)\n value = args[0]\n if isinstance(value, (str, list, dict)):\n return len(value) == 0\n raise BuiltinError(\n f'Функсияи \"холӣ_аст\" матн, рӯйхат ё луғат мехоҳад, вале '\n f\"{type_name(value)} гирифт.\"\n )\n\n # --- list operations ----------------------------------------------------\n def илова(*args: Any) -> list:\n _arity(\"илова\", args, 2)\n target = _list(\"илова\", args[0])\n target.append(args[1])\n return target\n\n def дарҷ(*args: Any) -> list:\n \"\"\"Дар ҷои муайян мегузорад: дарҷ(рӯйхат, 0, қимат).\"\"\"\n _arity(\"дарҷ\", args, 3)\n target = _list(\"дарҷ\", args[0])\n target.insert(_position(\"дарҷ\", args[1], len(target), allow_end=True), args[2])\n return target\n\n def хориҷ(*args: Any) -> Any:\n \"\"\"Унсурро аз рӯйхат мебарорад ва онро бармегардонад.\"\"\"\n _arity(\"хориҷ\", args, 2)\n target = _list(\"хориҷ\", args[0])\n if not target:\n raise BuiltinError('Функсияи \"хориҷ\" рӯйхати холиро қабул намекунад.')\n return target.pop(_position(\"хориҷ\", args[1], len(target)))\n\n def васеъ(*args: Any) -> list:\n \"\"\"Ҳамаи унсурҳои рӯйхати дуюмро илова мекунад.\"\"\"\n _arity(\"васеъ\", args, 2)\n target = _list(\"васеъ\", args[0])\n target.extend(_list(\"васеъ\", args[1]))\n return target\n\n def холӣ_кун(*args: Any) -> list:\n _arity(\"холӣ_кун\", args, 1)\n target = _list(\"холӣ_кун\", args[0])\n target.clear()\n return target\n\n def ҷои(*args: Any) -> Any:\n \"\"\"Рақами тартибии аввалин мувофиқат, ё «холӣ».\"\"\"\n _arity(\"ҷои\", args, 2)\n items = _list(\"ҷои\", args[0])\n for index, item in enumerate(items):\n if item == args[1] and type_name(item) == type_name(args[1]):\n return index\n return None\n\n def шумор(*args: Any) -> int:\n _arity(\"шумор\", args, 2)\n container = args[0]\n if isinstance(container, list):\n return sum(\n 1 for item in container\n if item == args[1] and type_name(item) == type_name(args[1])\n )\n if isinstance(container, str):\n return container.count(_text(\"шумор\", args[1]))\n raise BuiltinError(\n f'Функсияи \"шумор\" рӯйхат ё матн мехоҳад, вале '\n f\"{type_name(container)} гирифт.\"\n )\n\n def рақамҳо(*args: Any) -> list:\n \"\"\"рақамҳо(1, 5) -> [1, 2, 3, 4, 5] — ҳарду ҳадд дохил мешаванд.\"\"\"\n _arity(\"рақамҳо\", args, 1, 2)\n if len(args) == 1:\n start, end = 1, _whole(\"рақамҳо\", args[0])\n else:\n start = _whole(\"рақамҳо\", args[0])\n end = _whole(\"рақамҳо\", args[1])\n step = 1 if end >= start else -1\n return list(range(start, end + step, step))\n\n # --- ordering and aggregation ------------------------------------------\n def тартиб(*args: Any) -> list:\n _arity(\"тартиб\", args, 1)\n return _ordered(\"тартиб\", _list(\"тартиб\", args[0]))\n\n def тартиб_бо(*args: Any) -> list:\n \"\"\"тартиб_бо(рӯйхат, функсия) — функсия калиди ҳар унсурро медиҳад.\n\n Ин ҳамон чизест, ки барои тартиб додани рӯйхати луғатҳо лозим аст:\n тартиб_бо(донишҷӯён, калиди_ном)\n \"\"\"\n _arity(\"тартиб_бо\", args, 2)\n items = _list(\"тартиб_бо\", args[0])\n key_fn = args[1]\n\n if call is None or not callable_value(key_fn):\n raise BuiltinError(\n f'Функсияи \"тартиб_бо\" ҳамчун аргументи дуюм функсия мехоҳад, '\n f\"вале {type_name(key_fn)} гирифт.\"\n )\n\n keyed = [(call(key_fn, [item]), index, item) for index, item in enumerate(items)]\n keys = [key for key, _, _ in keyed]\n if all(isinstance(key, str) for key in keys):\n keyed.sort(key=lambda triple: (collation.sort_key(triple[0]), triple[1]))\n elif all(is_number(key) for key in keys):\n keyed.sort(key=lambda triple: (triple[0], triple[1]))\n else:\n raise BuiltinError(\n 'Функсияи \"тартиб_бо\" бояд ҳамеша матн ё ҳамеша рақам диҳад.'\n )\n return [item for _, _, item in keyed]\n\n def баръакс(*args: Any) -> Any:\n _arity(\"баръакс\", args, 1)\n value = args[0]\n if isinstance(value, list):\n return list(reversed(value))\n if isinstance(value, str):\n return value[::-1]\n raise BuiltinError(\n f'Функсияи \"баръакс\" рӯйхат ё матн мехоҳад, вале '\n f\"{type_name(value)} гирифт.\"\n )\n\n def ҷамъи(*args: Any) -> Any:\n _arity(\"ҷамъи\", args, 1)\n total: Any = 0\n for item in _list(\"ҷамъи\", args[0]):\n if not is_number(item):\n raise BuiltinError(\n f'Функсияи \"ҷамъи\" рӯйхати рақамҳоро мехоҳад, вале дар он '\n f\"{type_name(item)} ҳаст.\"\n )\n total = total + item\n return total\n\n def миёна(*args: Any) -> Any:\n _arity(\"миёна\", args, 1)\n items = _list(\"миёна\", args[0])\n if not items:\n raise BuiltinError(\n 'Функсияи \"миёна\" рӯйхати холиро қабул намекунад.',\n hint=\"Аввал санҷед: агар дарозӣ(рӯйхат) > 0:\",\n )\n return ҷамъи(items) / len(items)\n\n def калонтарин(*args: Any) -> Any:\n _arity(\"калонтарин\", args, 1)\n items = _list(\"калонтарин\", args[0])\n if not items:\n raise BuiltinError(\n 'Функсияи \"калонтарин\" рӯйхати холиро қабул намекунад.',\n hint=\"Аввал санҷед: агар дарозӣ(рӯйхат) > 0:\",\n )\n return _ordered(\"калонтарин\", items)[-1]\n\n def хурдтарин(*args: Any) -> Any:\n _arity(\"хурдтарин\", args, 1)\n items = _list(\"хурдтарин\", args[0])\n if not items:\n raise BuiltinError(\n 'Функсияи \"хурдтарин\" рӯйхати холиро қабул намекунад.',\n hint=\"Аввал санҷед: агар дарозӣ(рӯйхат) > 0:\",\n )\n return _ordered(\"хурдтарин\", items)[0]\n\n def ҳама(*args: Any) -> bool:\n \"\"\"Оё ҳамаи унсурҳо «рост» ҳастанд?\"\"\"\n _arity(\"ҳама\", args, 1)\n return all(_booleans(\"ҳама\", _list(\"ҳама\", args[0])))\n\n def ягон(*args: Any) -> bool:\n \"\"\"Оё ҳадди аққал яке «рост» аст?\"\"\"\n _arity(\"ягон\", args, 1)\n return any(_booleans(\"ягон\", _list(\"ягон\", args[0])))\n\n def _booleans(name: str, items: list) -> list[bool]:\n for item in items:\n if not isinstance(item, bool):\n raise BuiltinError(\n f'Функсияи \"{name}\" рӯйхати қиматҳои мантиқиро мехоҳад, '\n f\"вале дар он {type_name(item)} ҳаст.\"\n )\n return items\n\n # --- dictionary operations ---------------------------------------------\n def калидҳо(*args: Any) -> list:\n _arity(\"калидҳо\", args, 1)\n return list(_dict(\"калидҳо\", args[0]))\n\n def қиматҳо(*args: Any) -> list:\n _arity(\"қиматҳо\", args, 1)\n return list(_dict(\"қиматҳо\", args[0]).values())\n\n def ҷуфтҳо(*args: Any) -> list:\n \"\"\"Ҳар ҷуфт ҳамчун рӯйхати [калид, қимат].\"\"\"\n _arity(\"ҷуфтҳо\", args, 1)\n return [[key, value] for key, value in _dict(\"ҷуфтҳо\", args[0]).items()]\n\n def гирифтан(*args: Any) -> Any:\n \"\"\"гирифтан(луғат, калид, пешфарз) — бе хато, агар калид набошад.\"\"\"\n _arity(\"гирифтан\", args, 2, 3)\n mapping = _dict(\"гирифтан\", args[0])\n default = args[2] if len(args) == 3 else None\n return mapping.get(args[1], default)\n\n def нест_кун(*args: Any) -> Any:\n \"\"\"Калидро аз луғат мебарорад ва қимати онро бармегардонад.\"\"\"\n _arity(\"нест_кун\", args, 2)\n mapping = _dict(\"нест_кун\", args[0])\n if args[1] not in mapping:\n raise BuiltinError(\n f\"Калиди {to_tajik_string(args[1])} дар луғат нест.\",\n hint=\"Бо функсияи «дорад» санҷед: дорад(луғат, калид)\",\n )\n return mapping.pop(args[1])\n\n core = {\n \"навис\": навис, \"хонед\": хонед,\n \"ба_рақам\": ба_рақам, \"ба_матн\": ба_матн, \"навъ\": навъ,\n \"дарозӣ\": дарозӣ, \"дорад\": дорад, \"нусха\": нусха, \"холӣ_аст\": холӣ_аст,\n \"илова\": илова, \"дарҷ\": дарҷ, \"хориҷ\": хориҷ, \"васеъ\": васеъ,\n \"холӣ_кун\": холӣ_кун, \"ҷои\": ҷои, \"шумор\": шумор, \"рақамҳо\": рақамҳо,\n \"буриш\": _буриш,\n \"тартиб\": тартиб, \"тартиб_бо\": тартиб_бо, \"баръакс\": баръакс,\n \"ҷамъи\": ҷамъи, \"миёна\": миёна,\n \"калонтарин\": калонтарин, \"хурдтарин\": хурдтарин,\n \"ҳама\": ҳама, \"ягон\": ягон,\n \"калидҳо\": калидҳо, \"қиматҳо\": қиматҳо, \"ҷуфтҳо\": ҷуфтҳо,\n \"гирифтан\": гирифтан, \"нест_кун\": нест_кун,\n }\n\n names: dict[str, Builtin] = {\n name: Builtin(name, fn) for name, fn in core.items()\n }\n\n # Everything in the two modules is a global as well, so a beginner never\n # has to `ворид` anything to write an ordinary program. The modules stay\n # as namespaces over the very same objects.\n for module in (_build_math(), _build_text()):\n for name, value in module.items():\n if isinstance(value, Builtin):\n names.setdefault(name, value)\n\n return names\n\n\ndef callable_value(value: Any) -> bool:\n \"\"\"Is this something the interpreter can call?\"\"\"\n return callable(value) or type_name(value) == \"функсия\"\n\n\n# ---------------------------------------------------------------------------\n# built-in module: файл\n# ---------------------------------------------------------------------------\ndef _build_file(interpreter: Any) -> dict[str, Any]:\n base_dir = interpreter.base_dir\n\n \"\"\"Reading and writing text files, relative to the program's own folder.\n\n Not global, unlike everything else: touching the disk should be a visible\n decision, so it costs one `ворид файл`.\n \"\"\"\n\n def _resolve(name: str, value: Any) -> Path:\n text = _text(name, value)\n path = Path(text)\n if not path.is_absolute():\n path = base_dir / path\n return path\n\n def мавҷуд(*args: Any) -> bool:\n _arity(\"мавҷуд\", args, 1)\n return _resolve(\"мавҷуд\", args[0]).exists()\n\n def хондан(*args: Any) -> str:\n \"\"\"Тамоми файлро ҳамчун матн мехонад.\"\"\"\n _arity(\"хондан\", args, 1)\n path = _resolve(\"хондан\", args[0])\n try:\n return path.read_text(encoding=\"utf-8\")\n except FileNotFoundError:\n raise BuiltinError(\n f'Файли \"{args[0]}\" ёфт нашуд.',\n hint=\"Бо «файл.мавҷуд» санҷед: агар файл.мавҷуд(ном):\",\n ) from None\n except OSError as error:\n raise BuiltinError(f\"Файл хонда нашуд: {error.strerror}.\") from None\n\n def сатрҳо(*args: Any) -> list:\n \"\"\"Файлро ҳамчун рӯйхати сатрҳо мехонад, бе сатрҳои холии охир.\"\"\"\n _arity(\"сатрҳо\", args, 1)\n return хондан(args[0]).splitlines()\n\n def навиштан(*args: Any) -> None:\n \"\"\"Файлро менависад. Агар файл бошад, мазмуни кӯҳна нест мешавад.\"\"\"\n _arity(\"навиштан\", args, 2)\n _write(args[0], _text(\"навиштан\", args[1]), append=False)\n\n def илова_кардан(*args: Any) -> None:\n \"\"\"Ба охири файл илова мекунад.\"\"\"\n _arity(\"илова_кардан\", args, 2)\n _write(args[0], _text(\"илова_кардан\", args[1]), append=True)\n\n def _write(name: Any, text: str, append: bool) -> None:\n path = _resolve(\"навиштан\", name)\n try:\n # newline=\"\" so that \"\\n\" is written as \"\\n\" on every platform.\n # Without it Windows silently turns each one into \"\\r\\n\", and a\n # program that writes a file byte by byte gets back something it\n # did not ask for — which is exactly how a self-hosting compiler\n # stops matching its own output.\n with path.open(\n \"a\" if append else \"w\", encoding=\"utf-8\", newline=\"\"\n ) as handle:\n handle.write(text)\n except OSError as error:\n raise BuiltinError(\n f\"Файл навишта нашуд: {error.strerror}.\",\n hint=\"Шояд ҷузвдон вуҷуд надорад ё иҷозат нест.\",\n ) from None\n\n return {\n name: Builtin(name, fn)\n for name, fn in {\n \"мавҷуд\": мавҷуд, \"хондан\": хондан, \"сатрҳо\": сатрҳо,\n \"навиштан\": навиштан, \"илова_кардан\": илова_кардан,\n }.items()\n }\n\n\n# name -> builder. Each builder takes the importing file's folder, which only\n# `файл` actually uses.\ndef _build_python(interpreter: Any) -> dict[str, Any]:\n from . import interop # imported here so `питон` costs nothing unused\n\n return interop.build_module()\n\n\ndef _build_page(interpreter: Any) -> dict[str, Any]:\n from . import page\n\n return page.build_module(interpreter)\n\n\ndef _build_data(interpreter: Any) -> dict[str, Any]:\n from . import extras\n\n return extras.build_data()\n\n\ndef _build_http(interpreter: Any) -> dict[str, Any]:\n from . import extras\n\n return extras.build_http()\n\n\n# name -> builder. Each builder receives the interpreter, which is how `файл`\n# learns the program's folder and `саҳифа` learns how to call a click handler.\nBUILTIN_MODULES: dict[str, Callable[[Any], dict[str, Any]]] = {\n \"риёзӣ\": lambda interpreter: _build_math(),\n \"матн\": lambda interpreter: _build_text(),\n \"файл\": _build_file,\n \"питон\": _build_python,\n \"саҳифа\": _build_page,\n \"маълумот\": _build_data,\n \"интернет\": _build_http,\n}\n", "syntax_next.py": "\"\"\"TajikLang Next surface syntax.\n\nThe interpreter's AST is intentionally independent of spelling. This module\nturns the newer, brace-based TajikLang notation into the stable core grammar\nwhile the parser migration is underway. Students write the new syntax; the\nruntime still receives the same well-tested AST.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\n\n\n_DECLARE = re.compile(r\"^дода\\s+([\\w]+)\\s*<-\\s*(.+)$\", re.UNICODE)\n_UPDATE = re.compile(r\"^тағйир\\s+([\\w]+)\\s*<-\\s*(.+)$\", re.UNICODE)\n_SHOW = re.compile(r\"^нишон\\s+(.+)$\", re.UNICODE)\n_IF = re.compile(r\"^агар\\s+(.+?)\\s*\\{$\", re.UNICODE)\n_ELSE_IF = re.compile(r\"^дигар\\s+агар\\s+(.+?)\\s*\\{$\", re.UNICODE)\n\n\ndef is_next_syntax(source: str) -> bool:\n \"\"\"Avoid changing existing programs unless they use a Next-only form.\"\"\"\n return \"<-\" in source or bool(re.search(\n r\"(?m)^\\s*(?:дода|тағйир|нишон)\\b|^\\s*(?:агар|дигар)\\b.*\\{\\s*$\",\n source,\n ))\n\n\ndef normalize(source: str) -> str:\n \"\"\"Convert concise TajikLang Next statements to the core source grammar.\n\n Blocks use braces because a missing indentation level should never change a\n student's program. Opening braces must end a line; closing braces may be\n followed by ``дигар {`` or ``дигар агар ... {``.\n \"\"\"\n if not is_next_syntax(source):\n return source\n\n depth = 0\n result: list[str] = []\n for number, raw in enumerate(source.splitlines(), start=1):\n text = raw.strip()\n if not text or text.startswith(\"#\"):\n result.append(text)\n continue\n\n if text.startswith(\"}\"):\n depth -= 1\n if depth < 0:\n raise ValueError(f\"сатри {number}: қавси '}}' зиёдатӣ аст\")\n text = text[1:].strip()\n if not text:\n continue\n if text == \"дигар {\":\n result.append(\" \" * depth + \"вагарна:\")\n depth += 1\n continue\n else_if = _ELSE_IF.match(text)\n if else_if:\n result.append(\" \" * depth + f\"вагарна агар {else_if.group(1)}:\")\n depth += 1\n continue\n raise ValueError(f\"сатри {number}: пас аз '}}' танҳо 'дигар' иҷозат дорад\")\n\n if text == \"дигар {\":\n result.append(\" \" * depth + \"вагарна:\")\n depth += 1\n continue\n\n if_match = _IF.match(text)\n if if_match:\n result.append(\" \" * depth + f\"агар {if_match.group(1)}:\")\n depth += 1\n continue\n\n declaration = _DECLARE.match(text)\n update = _UPDATE.match(text)\n shown = _SHOW.match(text)\n if declaration:\n text = f\"бигзор {declaration.group(1)} = {declaration.group(2)}\"\n elif update:\n text = f\"{update.group(1)} = {update.group(2)}\"\n elif shown:\n text = f\"навис({shown.group(1)})\"\n elif \"<-\" in text:\n raise ValueError(\n f\"сатри {number}: барои қимати нав 'дода ном <- қимат' \"\n \"ва барои тағйир 'тағйир ном <- қимат' нависед\"\n )\n\n result.append(\" \" * depth + text)\n\n if depth:\n raise ValueError(\"блок бо '}' баста нашудааст\")\n return \"\\n\".join(result) + (\"\\n\" if source.endswith(\"\\n\") else \"\")\n", "tokens.py": "\"\"\"Токенҳо — the vocabulary of TajikLang.\n\nA token is the smallest meaningful piece of source code: a name, a number,\na string, an operator. The lexer's only job is to turn a stream of characters\ninto a stream of these.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom enum import Enum, auto\nfrom typing import Any\n\n\nclass TokenType(Enum):\n # --- literals and names ---\n NUMBER = auto() # 10, 3.5\n STRING = auto() # \"Салом\"\n IDENT = auto() # навис, ном, ҳарорат\n TRUE = auto() # рост\n FALSE = auto() # дурӯғ\n NULL = auto() # холӣ\n\n # --- keywords ---\n LET = auto() # бигзор\n AND = auto() # ва\n OR = auto() # ё\n NOT = auto() # не\n IF = auto() # агар\n ELSE = auto() # вагарна\n FOR = auto() # барои\n FROM = auto() # аз\n TO = auto() # то\n WHILE = auto() # то_вақте\n FUNC = auto() # функсия\n RETURN = auto() # баргардон\n IMPORT = auto() # ворид\n BREAK = auto() # шикан\n CONTINUE = auto() # давом\n TRY = auto() # кӯшиш\n CATCH = auto() # хато\n CLASS = auto() # қолиб\n INHERITS = auto() # мерос\n\n # --- arithmetic ---\n PLUS = auto() # +\n MINUS = auto() # -\n STAR = auto() # *\n SLASH = auto() # /\n PERCENT = auto() # %\n\n # --- comparison ---\n EQ = auto() # ==\n NEQ = auto() # !=\n LT = auto() # <\n GT = auto() # >\n LTE = auto() # <=\n GTE = auto() # >=\n\n # --- compound assignment ---\n PLUS_ASSIGN = auto() # +=\n MINUS_ASSIGN = auto() # -=\n STAR_ASSIGN = auto() # *=\n SLASH_ASSIGN = auto() # /=\n PERCENT_ASSIGN = auto() # %=\n\n # --- assignment and punctuation ---\n PIPE = auto() # |>\n ASSIGN = auto() # =\n LPAREN = auto() # (\n RPAREN = auto() # )\n LBRACKET = auto() # [\n RBRACKET = auto() # ]\n LBRACE = auto() # {\n RBRACE = auto() # }\n COMMA = auto() # ,\n COLON = auto() # :\n DOT = auto() # .\n\n # --- structure ---\n NEWLINE = auto() # end of a statement\n INDENT = auto() # a block opens (4 more spaces)\n DEDENT = auto() # a block closes\n EOF = auto() # end of file\n\n def __str__(self) -> str:\n return self.name\n\n\n# Keywords that currently mean something. The lexer turns these into their own\n# token type, so the parser never has to compare strings.\nKEYWORD_TOKENS: dict[str, TokenType] = {\n \"бигзор\": TokenType.LET,\n \"рост\": TokenType.TRUE,\n \"дурӯғ\": TokenType.FALSE,\n \"холӣ\": TokenType.NULL,\n \"ва\": TokenType.AND,\n \"ё\": TokenType.OR,\n \"не\": TokenType.NOT,\n \"агар\": TokenType.IF,\n \"вагарна\": TokenType.ELSE,\n \"барои\": TokenType.FOR,\n \"аз\": TokenType.FROM,\n \"то\": TokenType.TO,\n \"то_вақте\": TokenType.WHILE,\n \"функсия\": TokenType.FUNC,\n \"баргардон\": TokenType.RETURN,\n \"ворид\": TokenType.IMPORT,\n \"шикан\": TokenType.BREAK,\n \"давом\": TokenType.CONTINUE,\n \"кӯшиш\": TokenType.TRY,\n \"хато\": TokenType.CATCH,\n \"қолиб\": TokenType.CLASS,\n \"мерос\": TokenType.INHERITS,\n}\n\n\n# The full reserved vocabulary. Words absent from KEYWORD_TOKENS are reserved\n# but not implemented: the lexer rejects them with a \"coming in a later\n# version\" message, so a program written today cannot break when they land.\nKEYWORDS: frozenset[str] = frozenset(KEYWORD_TOKENS) | frozenset({\n})\n\n# `худ` (self) and `синф` (class) are deliberately NOT reserved. Python does\n# not reserve `self` either, and a school program wants `бигзор синф = 10` on\n# its first line — so the class keyword is `қолиб` (\"mould\"), which is also\n# the metaphor: a class is the mould an object is cast from.\n\n\n# Version each not-yet-implemented keyword is scheduled for.\nKEYWORD_VERSIONS: dict[str, str] = {}\n\n\n# Two-character operators, checked before single characters so that \"<=\" is\n# never read as \"<\" followed by \"=\".\nTWO_CHAR_OPERATORS: dict[str, TokenType] = {\n \"==\": TokenType.EQ,\n \"!=\": TokenType.NEQ,\n \"<=\": TokenType.LTE,\n \">=\": TokenType.GTE,\n \"+=\": TokenType.PLUS_ASSIGN,\n \"-=\": TokenType.MINUS_ASSIGN,\n \"*=\": TokenType.STAR_ASSIGN,\n \"/=\": TokenType.SLASH_ASSIGN,\n \"%=\": TokenType.PERCENT_ASSIGN,\n \"|>\": TokenType.PIPE,\n}\n\n\n# Compound assignment token -> the arithmetic operator it stands for.\n# `x += 1` is rewritten by the parser into `x = x + 1`, so the interpreter\n# never learns that compound assignment exists.\nCOMPOUND_OPERATORS: dict[TokenType, str] = {\n TokenType.PLUS_ASSIGN: \"+\",\n TokenType.MINUS_ASSIGN: \"-\",\n TokenType.STAR_ASSIGN: \"*\",\n TokenType.SLASH_ASSIGN: \"/\",\n TokenType.PERCENT_ASSIGN: \"%\",\n}\n\n\n# Single characters that form a complete token on their own.\nSINGLE_CHARS: dict[str, TokenType] = {\n \"(\": TokenType.LPAREN,\n \")\": TokenType.RPAREN,\n \"[\": TokenType.LBRACKET,\n \"]\": TokenType.RBRACKET,\n \"{\": TokenType.LBRACE,\n \"}\": TokenType.RBRACE,\n \",\": TokenType.COMMA,\n \".\": TokenType.DOT,\n \"+\": TokenType.PLUS,\n \"-\": TokenType.MINUS,\n \"*\": TokenType.STAR,\n \"/\": TokenType.SLASH,\n \"%\": TokenType.PERCENT,\n \"=\": TokenType.ASSIGN,\n \":\": TokenType.COLON,\n \"<\": TokenType.LT,\n \">\": TokenType.GT,\n}\n\n\n# Operators students arrive with from other languages, and what to write here\n# instead. Recognising them costs nothing and turns \"unknown character\" into a\n# usable answer.\nWRONG_OPERATORS: dict[str, str] = {\n \"&&\": 'Дар ин ҷо калимаи \"ва\" -ро истифода баред.',\n \"||\": 'Дар ин ҷо калимаи \"ё\" -ро истифода баред.',\n \"!\": 'Барои инкор калимаи \"не\" -ро истифода баред: не (x == 5)',\n \"===\": 'Барои муқоиса аломати \"==\" кифоя аст.',\n}\n\n\n@dataclass(frozen=True)\nclass Token:\n \"\"\"One token, plus where it came from.\n\n `line` and `column` are 1-based and are carried all the way through the\n parser and interpreter so that every error message can point at the exact\n character that caused it.\n \"\"\"\n\n type: TokenType\n value: Any\n line: int\n column: int\n\n def __repr__(self) -> str:\n return f\"{self.type}({self.value!r})@{self.line}:{self.column}\"\n", "values.py": "\"\"\"Қиматҳо — the runtime values of TajikLang, and how they are described.\n\nEverything a TajikLang program can hold lives here: the Python types it reuses\n(int, float, str, bool, None, list, dict) plus the three it needs of its own —\nbuilt-in functions, user functions and modules.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom decimal import Decimal, getcontext\nfrom typing import TYPE_CHECKING, Any, Callable\n\n# TajikLang uses **decimal** arithmetic, not binary floating point.\n#\n# In Python — and C, and Java, and JavaScript — `0.1 + 0.2` is\n# 0.30000000000000004, because 0.1 has no exact binary form. Every beginner\n# meets that, and no explanation of it belongs in lesson 3. School arithmetic\n# is decimal, money is decimal, marks are decimal; so is this language.\n#\n# 20 significant digits: enough that nothing a student writes runs out, few\n# enough that 1/3 does not fill a line.\ngetcontext().prec = 20\n\nif TYPE_CHECKING: # pragma: no cover - typing only\n from . import ast_nodes as ast\n\n\nclass Builtin:\n \"\"\"A function implemented in Python and exposed to TajikLang.\"\"\"\n\n def __init__(self, name: str, fn: Callable[..., Any]) -> None:\n self.name = name\n self.fn = fn\n\n def __call__(self, *args: Any) -> Any:\n return number(self.fn(*args))\n\n def __repr__(self) -> str:\n return f\"<функсияи дарунсохт {self.name}>\"\n\n\nclass Function:\n \"\"\"A function written in TajikLang.\n\n `closure` is the environment the function was *declared* in, not the one it\n is called from. That single choice is what makes TajikLang lexically\n scoped: a function always sees the names that surrounded its own text.\n \"\"\"\n\n def __init__(self, declaration: \"ast.FunctionDeclaration\", closure: Any) -> None:\n self.declaration = declaration\n self.closure = closure\n\n @property\n def name(self) -> str:\n return self.declaration.name\n\n @property\n def parameters(self) -> list[str]:\n return self.declaration.parameters\n\n def __repr__(self) -> str:\n params = \", \".join(self.parameters)\n return f\"<функсияи {self.name}({params})>\"\n\n\nclass TajikClass:\n \"\"\"A class: a name, an optional parent, and a bag of methods.\"\"\"\n\n def __init__(\n self,\n name: str,\n parent: \"TajikClass | None\",\n methods: dict[str, \"Function\"],\n ) -> None:\n self.name = name\n self.parent = parent\n self.methods = methods\n\n def find(self, name: str) -> \"Function | None\":\n \"\"\"Look here, then up the chain of parents.\"\"\"\n if name in self.methods:\n return self.methods[name]\n if self.parent is not None:\n return self.parent.find(name)\n return None\n\n def method_names(self) -> list[str]:\n names = list(self.methods)\n if self.parent is not None:\n names.extend(self.parent.method_names())\n return names\n\n def __repr__(self) -> str:\n return f\"<синфи {self.name}>\"\n\n\nclass Instance:\n \"\"\"One object made from a class.\"\"\"\n\n def __init__(self, cls: TajikClass) -> None:\n self.cls = cls\n self.fields: dict[str, Any] = {}\n\n def __repr__(self) -> str:\n return f\"<{self.cls.name}>\"\n\n\nclass BoundMethod:\n \"\"\"A method with its object already attached, so `худ` is filled in.\"\"\"\n\n def __init__(self, instance: Instance, function: \"Function\") -> None:\n self.instance = instance\n self.function = function\n\n @property\n def name(self) -> str:\n return self.function.name\n\n @property\n def parameters(self) -> list[str]:\n return self.function.parameters[1:] # `худ` is supplied for you\n\n def __repr__(self) -> str:\n return f\"<методи {self.instance.cls.name}.{self.function.name}>\"\n\n\nclass Module:\n \"\"\"A namespace: either a built-in module or another .tj file.\"\"\"\n\n def __init__(self, name: str, environment: Any) -> None:\n self.name = name\n self.environment = environment\n\n def __repr__(self) -> str:\n return f\"<модули {self.name}>\"\n\n\nclass ReturnSignal(Exception):\n \"\"\"`баргардон` unwinding the Python stack back to the call site.\n\n A tree-walk interpreter runs a `баргардон` deep inside nested `execute`\n calls, and there is no way to hand a value back up through them all except\n to raise. This is a control-flow signal, not an error — nothing catches it\n but `Function.call`.\n \"\"\"\n\n def __init__(self, value: Any) -> None:\n super().__init__(\"баргардон\")\n self.value = value\n\n\nclass BreakSignal(Exception):\n \"\"\"`шикан` unwinding out to the nearest loop. Control flow, not an error.\"\"\"\n\n\nclass ContinueSignal(Exception):\n \"\"\"`давом` unwinding out to the nearest loop. Control flow, not an error.\"\"\"\n\n\ndef is_number(value: Any) -> bool:\n # bool is a subclass of int in Python; booleans must not silently behave\n # as 1 and 0 in arithmetic.\n return isinstance(value, (int, float, Decimal)) and not isinstance(value, bool)\n\n\ndef is_whole(value: Any) -> bool:\n \"\"\"Is this number a whole one? Works for int, Decimal and float alike.\"\"\"\n if isinstance(value, bool) or not is_number(value):\n return False\n if isinstance(value, int):\n return True\n if isinstance(value, Decimal):\n return value == value.to_integral_value()\n return float(value).is_integer()\n\n\ndef number(value: Any) -> Any:\n \"\"\"Every number entering the language becomes int or Decimal, never float.\n\n Applied at each boundary — literals, arithmetic results, builtin return\n values, Python interop — because Decimal and float cannot be mixed in\n arithmetic, so a single stray float would fail far from its source.\n \"\"\"\n if isinstance(value, bool):\n return value\n if isinstance(value, float):\n return Decimal(repr(value))\n if isinstance(value, list):\n return [number(item) for item in value]\n if isinstance(value, dict):\n return {key: number(item) for key, item in value.items()}\n return value\n\n\ndef type_name(value: Any) -> str:\n \"\"\"The Tajik name of a value's type, for error messages.\"\"\"\n if isinstance(value, bool):\n return \"мантиқӣ\"\n if is_number(value):\n return \"рақам\"\n if isinstance(value, str):\n return \"матн\"\n if value is None:\n return \"холӣ\"\n if isinstance(value, list):\n return \"рӯйхат\"\n if isinstance(value, dict):\n return \"луғат\"\n if isinstance(value, Module):\n return \"модул\"\n if isinstance(value, TajikClass):\n return \"синф\"\n if isinstance(value, Instance):\n return value.cls.name # `навъ(д)` naming the class is useful\n if isinstance(value, BoundMethod):\n return \"функсия\"\n if isinstance(value, (Function, Builtin)) or callable(value):\n return \"функсия\"\n return \"номаълум\"\n\n\ndef to_tajik_string(value: Any) -> str:\n \"\"\"How a value looks when printed.\"\"\"\n if isinstance(value, bool):\n return \"рост\" if value else \"дурӯғ\"\n if isinstance(value, Decimal):\n if value == value.to_integral_value():\n return str(int(value)) # 4.0 prints as 4\n text = format(value.normalize(), \"f\")\n return text\n if isinstance(value, float) and value.is_integer():\n return str(int(value)) # 4.0 prints as 4\n if value is None:\n return \"холӣ\"\n if isinstance(value, str):\n return value\n if isinstance(value, list):\n return \"[\" + \", \".join(_repr_inside(item) for item in value) + \"]\"\n if isinstance(value, dict):\n inner = \", \".join(\n f\"{_repr_inside(key)}: {_repr_inside(item)}\" for key, item in value.items()\n )\n return \"{\" + inner + \"}\"\n if isinstance(value, Instance):\n # An object prints as its class plus its fields, so `навис(д)` during\n # debugging shows something worth reading.\n inner = \", \".join(\n f\"{key}: {_repr_inside(item)}\" for key, item in value.fields.items()\n )\n return f\"{value.cls.name}(\" + inner + \")\"\n return str(value)\n\n\ndef _repr_inside(value: Any) -> str:\n \"\"\"Like `to_tajik_string`, but strings keep their quotes.\n\n `навис(\"а\")` prints `а`, but `навис([\"а\"])` prints `[\"а\"]` — otherwise a\n list of strings and a list of names look identical.\n \"\"\"\n if isinstance(value, str):\n return '\"' + value + '\"'\n return to_tajik_string(value)\n\n\ndef equals(left: Any, right: Any) -> bool:\n \"\"\"Equality that never fails — different kinds are simply unequal.\n\n The kind check matters because Python treats `True == 1` as true, and a\n student who writes `рост == 1` should get `дурӯғ`, not a surprise. Lists\n and dictionaries compare element by element under the same rule.\n \"\"\"\n if type_name(left) != type_name(right):\n return False\n\n if isinstance(left, list):\n return len(left) == len(right) and all(\n equals(a, b) for a, b in zip(left, right)\n )\n\n if isinstance(left, dict):\n if set(left) != set(right):\n return False\n return all(equals(left[key], right[key]) for key in left)\n\n return left == right\n", "web.py": "\"\"\"TajikWeb — turn a TajikLang page into a small static website.\n\nThis deliberately starts with static pages. A portfolio, school project,\nclass journal, documentation site, or landing page should be publishable\nwithout teaching a beginner JavaScript, a web server, or deployment tools.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport shutil\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nfrom . import CheckFailed, run_source\nfrom .errors import TajikLangError\n\n\nPROJECT_FILE = \"tajikweb.json\"\nSOURCE_FILE = \"index.tj\"\nOUTPUT_DIR = \"нашр\"\nSTATIC_DIR = \"static\"\n\n\nclass WebError(Exception):\n \"\"\"An expected TajikWeb problem, ready for the command line to show.\"\"\"\n\n\n@dataclass(frozen=True)\nclass BuildResult:\n root: Path\n output: Path\n copied_assets: int\n\n\ndef _project_path(value: str | Path | None) -> Path:\n return Path.cwd() if value is None else Path(value)\n\n\ndef _valid_name(name: str) -> bool:\n \"\"\"Folders may be Tajik, but not contain path separators or hidden paths.\"\"\"\n return bool(re.fullmatch(r\"[^\\\\/:*?\\\"<>|.][^\\\\/:*?\\\"<>|]*\", name))\n\n\ndef create_project(name: str, parent: str | Path | None = None) -> Path:\n \"\"\"Create one teachable TajikWeb project without overwriting anything.\"\"\"\n if not _valid_name(name):\n raise WebError(\n \"Номи сомона нодуруст аст. Номи кӯтоҳ бе /, \\\\ ё аломатҳои махсус диҳед.\"\n )\n\n root = _project_path(parent) / name\n if root.exists():\n raise WebError(f'Папкаи \"{root}\" аллакай вуҷуд дорад.')\n\n root.mkdir(parents=True)\n (root / STATIC_DIR).mkdir()\n (root / PROJECT_FILE).write_text(\n json.dumps({\"ном\": name, \"навъ\": \"TajikWeb\", \"нусха\": 1}, ensure_ascii=False, indent=2)\n + \"\\n\",\n encoding=\"utf-8\",\n )\n (root / SOURCE_FILE).write_text(_starter_source(name), encoding=\"utf-8\")\n (root / \"README.md\").write_text(_starter_readme(name), encoding=\"utf-8\")\n return root\n\n\ndef build_project(folder: str | Path | None = None) -> BuildResult:\n \"\"\"Build ``index.tj`` in *folder* as a portable static website.\"\"\"\n root = _project_path(folder).resolve()\n project = root / PROJECT_FILE\n source_path = root / SOURCE_FILE\n if not project.is_file():\n raise WebError(\n f'Дар \"{root}\" файли {PROJECT_FILE} нест. '\n \"Аввал: tajik веб нав номи_сомона\"\n )\n if not source_path.is_file():\n raise WebError(f'Файли {SOURCE_FILE} ёфт нашуд.')\n\n try:\n source = source_path.read_text(encoding=\"utf-8\")\n except UnicodeDecodeError as error:\n raise WebError(f'Файли {SOURCE_FILE} бояд UTF-8 бошад.') from error\n\n # ``нависед`` keeps page elements in the module. Asking for HTML after\n # the student's source ran is intentionally invisible to their program.\n output: list[str] = []\n rendered_source = source.rstrip() + \"\\n\\nнавис(саҳифа.ҳамчун_матн())\\n\"\n try:\n run_source(\n rendered_source,\n filename=str(source_path),\n output=output.append,\n base_dir=root,\n check=True,\n )\n except CheckFailed as error:\n raise WebError(error.format()) from error\n except TajikLangError as error:\n raise WebError(error.format()) from error\n\n if not output or not output[-1].lstrip().lower().startswith(\" Path:\n \"\"\"Add a GitHub Actions workflow that builds and publishes this website.\"\"\"\n root = _project_path(folder).resolve()\n if not (root / PROJECT_FILE).is_file():\n raise WebError(f'Дар \"{root}\" файли {PROJECT_FILE} нест.')\n\n workflow = root / \".github\" / \"workflows\" / \"tajikweb-pages.yml\"\n workflow.parent.mkdir(parents=True, exist_ok=True)\n workflow.write_text(GITHUB_PAGES_WORKFLOW, encoding=\"utf-8\")\n return workflow\n\n\ndef _starter_source(name: str) -> str:\n return f'''# Сомонаи аввалини ман бо TajikWeb\nворид саҳифа\n\nсаҳифа.нависед(\n саҳифа.қуттӣ(\n саҳифа.қатор(\n саҳифа.матн(\"{name}\", {{\"ғафсӣ\": 800, \"андоза\": 19, \"ҳошия\": 0}}),\n саҳифа.пайванд(\"Дар бораи ман\", \"#дар-бора\"),\n саҳифа.пайванд(\"Тамос\", \"#тамос\"),\n {{\"justify-content\": \"space-between\", \"margin\": \"0 0 56px\"}},\n ),\n саҳифа.сарлавҳа(\"Салом — ман бо TajikLang месозам\", {{\"андоза\": 52, \"ранг\": \"#093c33\", \"ҳошия\": \"0 0 18px\"}}),\n саҳифа.матн(\"Ин сомона аз файли оддии тоҷикӣ сохта шудааст. Матн, ранг ва тарҳро худатон иваз кунед.\", {{\"андоза\": 20, \"васеъӣ\": \"min(640px, 100%)\", \"ҳошия\": \"0 0 26px\"}}),\n саҳифа.қатор(\n саҳифа.пайванд(\"Лоиҳаро бинед →\", \"#лоиҳаҳо\", {{\"замина\": \"#087f65\", \"ранг\": \"white\", \"фосила\": \"12px 18px\", \"гӯшаҳо\": 10, \"text-decoration\": \"none\", \"ғафсӣ\": 700}}),\n саҳифа.пайванд(\"Дар бораи ман\", \"#дар-бора\", {{\"фосила\": \"12px 18px\", \"канор\": \"1px solid #a9d5c8\", \"гӯшаҳо\": 10, \"text-decoration\": \"none\"}}),\n {{\"margin\": \"0 0 72px\", \"flex-wrap\": \"wrap\"}},\n ),\n саҳифа.қуттӣ(\n саҳифа.сарлавҳа(\"Дар бораи ман\", 2),\n саҳифа.матн(\"Дар ин ҷо ҳикояи кӯтоҳи худ, маҳорати худ ва он чизеро, ки мехоҳед созед, нависед.\"),\n саҳифа.рӯйхат([\"Омӯхтан\", \"Сохтан\", \"Нашр кардан\"]),\n {{\"замина\": \"white\", \"фосила\": 28, \"гӯшаҳо\": 18, \"канор\": \"1px solid #dce9e5\", \"max-width\": \"780px\"}},\n ),\n {{\"max-width\": \"1120px\", \"margin\": \"0 auto\", \"padding\": \"44px 24px\"}},\n ),\n)\n'''\n\n\ndef _starter_readme(name: str) -> str:\n return f'''# {name}\n\nИн сомона бо **TajikWeb** сохта мешавад.\n\n1. Матнро дар `index.tj` тағйир диҳед.\n2. Барои сохтани HTML: `tajik веб соз`\n3. Файли тайёр дар `нашр/index.html` аст.\n4. Барои GitHub Pages: `tajik веб омода-github`, баъд лоиҳаро ба GitHub push кунед.\n\nФайлҳоеро монанди расм ба папкаи `static/` гузоред ва дар саҳифа ҳамчун\n`static/номи-расм.png` истифода баред.\n'''\n\n\nGITHUB_PAGES_WORKFLOW = \"\"\"# TajikWeb сомонаро баъд аз ҳар push ба GitHub Pages нашр мекунад.\nname: Нашри TajikWeb\n\non:\n push:\n branches: [main]\n workflow_dispatch:\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\nconcurrency:\n group: pages\n cancel-in-progress: false\n\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.12\"\n - name: Насби TajikLang\n run: pip install git+https://github.com/BahromPython/tajiklang.git\n - name: Сохтани сомона\n run: tajik веб соз .\n - uses: actions/upload-pages-artifact@v3\n with:\n path: нашр\n\n deploy:\n environment:\n name: github-pages\n url: ${{{{ steps.deployment.outputs.page_url }}}}\n needs: build\n runs-on: ubuntu-latest\n steps:\n - id: deployment\n uses: actions/deploy-pages@v4\n\"\"\"\n", "webapp.py": "\"\"\"TajikWeb App — interactive TajikLang pages on localhost.\n\nStatic TajikWeb sites are ideal for GitHub Pages. A page with buttons and\nforms needs a live interpreter, so this runner keeps one interpreter in memory\nand exposes it only on 127.0.0.1. The browser never receives Python code or a\nnetwork-facing server.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport webbrowser\nfrom http import HTTPStatus\nfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\nfrom pathlib import Path\nfrom urllib.parse import urlparse\n\nfrom .errors import TajikLangError\nfrom .interpreter import Interpreter\nfrom .lexer import Lexer\nfrom .parser import Parser\n\n\nclass AppError(Exception):\n pass\n\n\nclass WebApp:\n def __init__(self, source_path: Path) -> None:\n self.source_path = source_path\n self.output: list[str] = []\n source = source_path.read_text(encoding=\"utf-8\")\n lexer = Lexer(source, str(source_path))\n program = Parser(lexer.tokenize(), lexer.lines).parse()\n self.interpreter = Interpreter(lexer.lines, self.output.append, base_dir=source_path.parent)\n try:\n self.interpreter.run(program)\n except TajikLangError as error:\n raise AppError(error.format()) from error\n if not hasattr(self.interpreter, \"page_html\"):\n raise AppError(\"Дар барнома «ворид саҳифа» ва «саҳифа.нависед(...)» лозим аст.\")\n\n def page(self) -> str:\n html = self.interpreter.page_html()\n script = \"\"\"\"\"\"\n return html.replace(\"\", script + \"\")\n\n def action(self, name: str) -> str:\n handler = getattr(self.interpreter, \"page_actions\", {}).get(name)\n if handler is None:\n raise AppError(\"Ин амал дар саҳифа нест.\")\n try:\n self.interpreter.call_from_host(handler, [])\n except TajikLangError as error:\n raise AppError(error.format()) from error\n return self.page()\n\n\ndef make_handler(app: WebApp):\n class Handler(BaseHTTPRequestHandler):\n server_version = \"TajikWeb\"\n sys_version = \"\"\n\n def log_message(self, _format: str, *_args: object) -> None:\n pass\n\n def _json(self, data: dict[str, str], status: HTTPStatus = HTTPStatus.OK) -> None:\n payload = json.dumps(data, ensure_ascii=False).encode(\"utf-8\")\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(payload)))\n self.end_headers()\n self.wfile.write(payload)\n\n def do_GET(self) -> None: # noqa: N802\n if urlparse(self.path).path != \"/\":\n self.send_error(HTTPStatus.NOT_FOUND)\n return\n payload = app.page().encode(\"utf-8\")\n self.send_response(HTTPStatus.OK)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(payload)))\n self.end_headers()\n self.wfile.write(payload)\n\n def do_POST(self) -> None: # noqa: N802\n prefix = \"/амал/\"\n path = urlparse(self.path).path\n if not path.startswith(prefix):\n self.send_error(HTTPStatus.NOT_FOUND)\n return\n try:\n self._json({\"html\": app.action(path[len(prefix):]), \"error\": \"\"})\n except AppError as error:\n self._json({\"html\": \"\", \"error\": str(error)}, HTTPStatus.BAD_REQUEST)\n\n return Handler\n\n\ndef start(source_path: Path, port: int = 8010, open_browser: bool = True) -> None:\n app = WebApp(source_path)\n server = ThreadingHTTPServer((\"127.0.0.1\", port), make_handler(app))\n address = f\"http://127.0.0.1:{port}/\"\n print(f\"TajikWeb App омода аст: {address}\")\n print(\"Барои қатъ кардан Ctrl+C-ро пахш кунед.\")\n if open_browser:\n webbrowser.open(address)\n try:\n server.serve_forever()\n except KeyboardInterrupt:\n print(\"\\nСервер қатъ шуд.\")\n finally:\n server.server_close()\n"}; const EXAMPLES = {"order": [["salom.tj", "1. Салом Тоҷикистон"], ["тағйирёбанда.tj", "2. Тағйирёбандаҳо"], ["муқоиса.tj", "3. Муқоиса ва мантиқ"], ["шарт.tj", "4. Шартҳо"], ["давр.tj", "5. Даврҳо"], ["функсия.tj", "6. Функсияҳо"], ["рӯйхат.tj", "7. Рӯйхат ва луғат"], ["матн_кор.tj", "8. Кор бо матн"], ["саволнома.tj", "9. Саволнома (хонед)"], ["санҷиш.tj", "10. Санҷиш (шикан, кӯшиш)"], ["журнал.tj", "11. Журнали баҳоҳо"], ["саҳифа_демо.tj", "12. Саҳифаи интернетӣ"], ["қолиб.tj", "13. Қолибҳо (синфҳо)"], ["чаро_ва_қувват.tj", "14. чаро, |> ва хатоҳо"]], "sources": {"salom.tj": "# Аввалин барномаи ман бо забони тоҷикӣ\nнавис(\"Салом Тоҷикистон!\")\n", "тағйирёбанда.tj": "# Тағйирёбандаҳо ва арифметика\n\nбигзор ном = \"Баҳром\"\nбигзор синну_сол = 16\n\nнавис(\"Салом,\", ном)\nнавис(\"Синну сол:\", синну_сол)\n\n# Арифметика\nбигзор паҳно = 4\nбигзор дарозӣ = 7.5\nнавис(\"Масоҳат:\", паҳно * дарозӣ)\n\n# Тартиби амалҳо: * пеш аз +\nнавис(2 + 3 * 4)\nнавис((2 + 3) * 4)\n\n# Тағйир додани қимат бе \"бигзор\"\nсинну_сол = синну_сол + 1\nнавис(\"Соли оянда:\", синну_сол)\n", "муқоиса.tj": "# Муқоиса ва мантиқ\n\nбигзор синну_сол = 16\n\nнавис(\"Синну сол:\", синну_сол)\nнавис(\"16 ё зиёдтар?\", синну_сол >= 16)\nнавис(\"Байни 10 ва 20?\", синну_сол > 10 ва синну_сол < 20)\nнавис(\"Хурд нест?\", не (синну_сол < 10))\n\n# Қиматҳои мантиқӣ\nнавис(рост, дурӯғ, холӣ)\n\n# Навъҳои гуногун ҳеҷ гоҳ баробар нестанд\nнавис(\"рост == 1 ->\", рост == 1)\nнавис(\"рақам == матн ->\", 1 == \"1\")\n\n# Тартиби алифбои тоҷикӣ, на тартиби Unicode\nнавис(\"Ғафуров пеш аз Яқубов?\", \"Ғафуров\" < \"Яқубов\")\nнавис(\"г пеш аз ғ?\", \"г\" < \"ғ\")\n", "шарт.tj": "# Шартҳо: агар / вагарна агар / вагарна\n\nбигзор синну_сол = 16\nбигзор мақом = \"\"\n\nагар синну_сол >= 18:\n мақом = \"калонсол\"\nвагарна агар синну_сол >= 16:\n мақом = \"наврас\"\n навис(\"Шумо метавонед барномасозиро омӯзед\")\nвагарна:\n мақом = \"хурд\"\n\nнавис(\"Мақом:\", мақом)\n\n# Блокҳои дарунӣ\nагар синну_сол > 10 ва синну_сол < 20:\n навис(\"Синну сол байни 10 ва 20 аст\")\n\n агар синну_сол % 2 == 0:\n навис(\"Синну сол ҷуфт аст\")\n вагарна:\n навис(\"Синну сол тоқ аст\")\n\nнавис(\"Барнома тамом шуд\")\n", "давр.tj": "# Даврҳо: барои ва то_вақте\n\n# Ҳарду ҳадд дохил мешаванд: аз 1 то 5 панҷ маротиба кор мекунад\nбарои i аз 1 то 5:\n навис(i)\n\nнавис(\"---\")\n\n# Ҳисоби ҷамъ\nбигзор ҷамъ = 0\nбарои i аз 1 то 10:\n ҷамъ = ҷамъ + i\nнавис(\"Ҷамъи 1..10:\", ҷамъ)\n\n# Ҳисоб ба поён\nбарои i аз 3 то 1:\n навис(\"шумориш:\", i)\n\n# Давр бо шарт\nбигзор ҳисоб = 1\nто_вақте ҳисоб <= 3:\n навис(\"такрор\", ҳисоб)\n ҳисоб = ҳисоб + 1\n\n# Ҷадвали зарб\nбарои сатр аз 1 то 3:\n барои сутун аз 1 то 3:\n навис(сатр, \"x\", сутун, \"=\", сатр * сутун)\n", "функсия.tj": "# Функсияҳо\n\nфунксия ҷамъ(a, b):\n баргардон a + b\n\nнавис(\"ҷамъ(5, 7) =\", ҷамъ(5, 7))\n\n# Функсия бе баргардон қимати «холӣ» медиҳад\nфунксия салом(ном):\n навис(\"Салом,\", ном)\n\nсалом(\"Баҳром\")\n\n# Рекурсия — ҳолати хотима ҳатмист\nфунксия факториал(n):\n агар n <= 1:\n баргардон 1\n баргардон n * факториал(n - 1)\n\nбарои i аз 1 то 6:\n навис(i, \"! =\", факториал(i))\n\n# Функсияҳо худашон қимат ҳастанд\nфунксия дучанд(x):\n баргардон x * 2\n\nбигзор амал = дучанд\nнавис(\"амал(21) =\", амал(21))\n\n# Функсия номҳои атрофи худро мебинад (closure)\nбигзор зарб = 3\nфунксия сегона(x):\n баргардон x * зарб\n\nнавис(\"сегона(7) =\", сегона(7))\n", "рӯйхат.tj": "# Рӯйхат ва луғат\n\nбигзор номҳо = [\"Яқубов\", \"Ғафуров\", \"Аҳмадов\", \"Ҷӯраев\"]\n\nнавис(\"Ҳама:\", номҳо)\nнавис(\"Дарозӣ:\", дарозӣ(номҳо))\nнавис(\"Якум:\", номҳо[0])\n\n# Тартиб бо алифбои тоҷикӣ, на бо рамзҳои Unicode\nнавис(\"Тартиб:\", тартиб(номҳо))\n\nилова(номҳо, \"Каримов\")\nномҳо[0] = \"Яқубова\"\nнавис(\"Пас аз тағйир:\", номҳо)\n\nбарои ном аз номҳо:\n навис(\"-\", ном)\n\nнавис(\"---\")\n\n# Луғат\nбигзор донишҷӯ = {\"ном\": \"Баҳром\", \"синф\": 10, \"баҳо\": 5}\n\nнавис(донишҷӯ[\"ном\"], \"аз синфи\", донишҷӯ[\"синф\"])\nдонишҷӯ[\"шаҳр\"] = \"Душанбе\"\n\nнавис(\"Калидҳо:\", калидҳо(донишҷӯ))\nнавис(\"Шаҳр дорад?\", дорад(донишҷӯ, \"шаҳр\"))\n\nбарои калид аз донишҷӯ:\n навис(калид, \"->\", донишҷӯ[калид])\n\n# Навъи ҳар қимат\nнавис(навъ(1), навъ(\"а\"), навъ(рост), навъ([1]), навъ({}), навъ(холӣ))\n", "матн_кор.tj": "# Кор бо матн\n\nворид матн\n\nбигзор ҷумла = \" Донишҷӯёни Тоҷикистон \"\n\nнавис(\"Аслӣ:\", ҷумла)\nнавис(\"Тоза:\", матн.тоза(ҷумла))\nнавис(\"Калон:\", матн.калон(матн.тоза(ҷумла)))\nнавис(\"Дарозӣ:\", дарозӣ(матн.тоза(ҷумла)))\n\nбигзор калимаҳо = матн.ҷудо(матн.тоза(ҷумла))\nнавис(\"Калимаҳо:\", калимаҳо)\nнавис(\"Шумораи калимаҳо:\", дарозӣ(калимаҳо))\n\nнавис(\"---\")\n\nбигзор шаҳрҳо = \"Душанбе,Хуҷанд,Кӯлоб,Бохтар\"\nбарои шаҳр аз матн.ҷудо(шаҳрҳо, \",\"):\n навис(\"-\", шаҳр, \"(\" + ба_матн(дарозӣ(шаҳр)) + \" ҳарф)\")\n\nнавис(\"---\")\n\nнавис(матн.иваз(\"салом-салом\", \"салом\", \"хайр\"))\nнавис(матн.буриш(\"Тоҷикистон\", 0, 3))\nнавис(матн.такрор(\"=\", 20))\nнавис(баръакс(\"Салом\"))\n", "саволнома.tj": "# Саволнома — бо функсияи «хонед» маълумот мегирем\n\nворид матн\nворид риёзӣ\n\nнавис(\"Салом! Ман якчанд савол медиҳам.\")\n\nбигзор ном = матн.тоза(хонед(\"Номи шумо чист? \"))\nбигзор ҷавоб = матн.тоза(хонед(\"Синну соли шумо чанд аст? \"))\n\nагар не матн.рақам_аст(ҷавоб):\n навис(\"Ин рақам нест:\", ҷавоб)\nвагарна:\n бигзор сол = ба_рақам(ҷавоб)\n\n навис(\"---\")\n навис(\"Салом,\", матн.калон(ном) + \"!\")\n навис(\"Соли оянда шумо\", сол + 1, \"мешавед.\")\n\n агар сол >= 16:\n навис(\"Шумо метавонед барномасозиро омӯзед.\")\n вагарна:\n навис(\"Барномасозӣ дар ҳар синну сол оғоз мешавад!\")\n\n бигзор рӯзҳо = сол * 365\n навис(\"Шумо тақрибан\", рӯзҳо, \"рӯз умр дидед.\")\n навис(\"Решаи синну соли шумо:\", риёзӣ.гирд(риёзӣ.реша(сол), 2))\n", "санҷиш.tj": "# Санҷиши маълумот — шикан, давом, кӯшиш ва хато\n\nбигзор ададҳо = [\"12\", \"салом\", \"7\", \"-3\", \"0\", \"99\"]\n\nнавис(\"Ҳамаи қиматҳо:\")\nбарои матни_адад аз ададҳо:\n кӯшиш:\n бигзор адад = ба_рақам(матни_адад)\n навис(\" \", матни_адад, \"->\", адад)\n хато паём:\n навис(\" \", матни_адад, \"-> хато:\", паём)\n\nнавис(такрор(\"-\", 40))\n\n# шикан: то аввалин қимати нодуруст меҳисобем\nбигзор ҷамъ = 0\nбарои матни_адад аз ададҳо:\n агар не рақам_аст(матни_адад):\n навис(\"Дар\", матни_адад, \"истодем.\")\n шикан\n ҷамъ += ба_рақам(матни_адад)\n\nнавис(\"Ҷамъ то истодан:\", ҷамъ)\n\nнавис(такрор(\"-\", 40))\n\n# давом: қиматҳои нодурустро мепартоем\nбигзор дурустҳо = []\nбарои матни_адад аз ададҳо:\n агар не рақам_аст(матни_адад):\n давом\n илова(дурустҳо, ба_рақам(матни_адад))\n\nнавис(\"Дурустҳо:\", дурустҳо)\nнавис(\"Ҷамъ:\", ҷамъи(дурустҳо))\nнавис(\"Миёна:\", гирд(миёна(дурустҳо), 2))\nнавис(\"Аз хурд ба калон:\", тартиб(дурустҳо))\nнавис(\"Охирин:\", дурустҳо[-1])\n", "журнал.tj": "# Журнали баҳоҳо — рӯйхат, луғат, функсия ва давр дар як барнома\n# Ҳеҷ \"ворид\" лозим нест: ҳамаи функсияҳо аз аввал дастрасанд.\n\nбигзор донишҷӯён = [\n {\"ном\": \"Яқубов\", \"баҳоҳо\": [5, 4, 5, 5]},\n {\"ном\": \"Ғафуров\", \"баҳоҳо\": [3, 4, 4, 3]},\n {\"ном\": \"Аҳмадов\", \"баҳоҳо\": [5, 5, 5, 4]},\n {\"ном\": \"Ҷӯраев\", \"баҳоҳо\": [2, 3, 3, 4]},\n]\n\nфунксия хулоса(миёнаи_баҳо):\n агар миёнаи_баҳо >= 4.5:\n баргардон \"аъло\"\n вагарна агар миёнаи_баҳо >= 3.5:\n баргардон \"хуб\"\n вагарна агар миёнаи_баҳо >= 2.5:\n баргардон \"қаноатбахш\"\n баргардон \"суст\"\n\nфунксия номи_донишҷӯ(д):\n баргардон д[\"ном\"]\n\nфунксия баҳои_донишҷӯ(д):\n баргардон миёна(д[\"баҳоҳо\"])\n\nнавис(\"Журнали синфи 10\")\nнавис(такрор(\"=\", 40))\n\n# тартиб_бо номҳоро бо алифбои тоҷикӣ ҷобаҷо мекунад:\n# Ғ баъд аз Г меояд, на баъд аз Я\nбарои д аз тартиб_бо(донишҷӯён, номи_донишҷӯ):\n бигзор натиҷа = миёна(д[\"баҳоҳо\"])\n навис(д[\"ном\"], \"|\", гирд(натиҷа, 2), \"|\", хулоса(натиҷа))\n\nнавис(такрор(\"=\", 40))\n\n# Беҳтарин донишҷӯ — охирин дар тартиби баҳоҳо\nбигзор аз_рӯи_баҳо = тартиб_бо(донишҷӯён, баҳои_донишҷӯ)\nбигзор беҳтарин = аз_рӯи_баҳо[-1]\nнавис(\"Беҳтарин:\", беҳтарин[\"ном\"])\n\nбигзор ҳама_баҳоҳо = []\nбарои д аз донишҷӯён:\n васеъ(ҳама_баҳоҳо, д[\"баҳоҳо\"])\n\nнавис(\"Миёнаи синф:\", гирд(миёна(ҳама_баҳоҳо), 2))\nнавис(\"Шумораи баҳоҳо:\", дарозӣ(ҳама_баҳоҳо))\nнавис(\"Баландтарин баҳо:\", калонтарин(ҳама_баҳоҳо))\n", "саҳифа_демо.tj": "# Саҳифаи интернетӣ — бо TajikLang\n#\n# Дар браузер: саҳифа зинда сохта мешавад ва тугмаҳо кор мекунанд.\n# Дар терминал: «саҳифа.ҳамчун_матн» матни HTML медиҳад,\n# ки онро бо «файл.навиштан» нигоҳ доштан мумкин аст.\n\nворид саҳифа\n\nбигзор донишҷӯён = [\n {\"ном\": \"Аҳмадов\", \"баҳо\": 4.75},\n {\"ном\": \"Ғафуров\", \"баҳо\": 3.5},\n {\"ном\": \"Ҷӯраев\", \"баҳо\": 3},\n {\"ном\": \"Яқубов\", \"баҳо\": 4.75},\n]\n\nфунксия номи(д):\n баргардон д[\"ном\"]\n\nфунксия баҳои(д):\n баргардон д[\"баҳо\"]\n\nфунксия беҳтаринро_нишон_деҳ():\n бигзор тартибдода = тартиб_бо(донишҷӯён, баҳои)\n бигзор беҳтарин = тартибдода[-1]\n саҳифа.нависед(\n саҳифа.услуб(\n саҳифа.матн(\"Беҳтарин:\", беҳтарин[\"ном\"], \"—\", беҳтарин[\"баҳо\"]),\n {\"ранг\": \"#0f7b5f\", \"ғафсӣ\": \"bold\"},\n ),\n )\n\nфунксия ҳисобро_нишон_деҳ():\n бигзор ҳама = []\n барои д аз донишҷӯён:\n илова(ҳама, д[\"баҳо\"])\n саҳифа.нависед(\n саҳифа.матн(\"Миёнаи синф:\", гирд(миёна(ҳама), 2)),\n )\n\nсаҳифа.нависед(\n саҳифа.сарлавҳа(\"Журнали синфи 10\"),\n саҳифа.матн(\"Ҳамагӣ\", дарозӣ(донишҷӯён), \"донишҷӯ.\"),\n саҳифа.ҷадвал(тартиб_бо(донишҷӯён, номи), [\"ном\", \"баҳо\"]),\n саҳифа.қатор(\n саҳифа.тугма(\"Беҳтаринро нишон деҳ\", беҳтаринро_нишон_деҳ),\n саҳифа.тугма(\"Миёнаро ҳисоб кун\", ҳисобро_нишон_деҳ),\n ),\n)\n", "қолиб.tj": "# Қолибҳо (синфҳо) — маълумот ва амалҳоро якҷоя нигоҳ медоранд\n#\n# Қолиб мисли қолиби хишт аст: аз як қолиб чандин объект сохта мешавад.\n\nқолиб Одам:\n функсия оғоз(худ, ном, шаҳр):\n худ.ном = ном\n худ.шаҳр = шаҳр\n\n функсия салом(худ):\n баргардон \"Салом, ман \" + худ.ном + \" аз \" + худ.шаҳр\n\n# «мерос» — қолиби нав ҳамаи функсияҳои волидро мегирад\nқолиб Донишҷӯ мерос Одам:\n функсия оғоз(худ, ном, шаҳр, баҳоҳо):\n худ.ном = ном\n худ.шаҳр = шаҳр\n худ.баҳоҳо = баҳоҳо\n\n функсия миёнаи_баҳо(худ):\n агар холӣ_аст(худ.баҳоҳо):\n баргардон 0\n баргардон миёна(худ.баҳоҳо)\n\n функсия хулоса(худ):\n бигзор м = худ.миёнаи_баҳо()\n агар м >= 4.5:\n баргардон \"аъло\"\n вагарна агар м >= 3.5:\n баргардон \"хуб\"\n баргардон \"қаноатбахш\"\n\nбигзор донишҷӯён = [\n Донишҷӯ(\"Яқубов\", \"Душанбе\", [5, 4, 5, 5]),\n Донишҷӯ(\"Ғафуров\", \"Хуҷанд\", [3, 4, 4, 3]),\n Донишҷӯ(\"Аҳмадов\", \"Кӯлоб\", [5, 5, 5, 4]),\n]\n\nфунксия номи(д):\n баргардон д.ном\n\nфунксия баҳои(д):\n баргардон д.миёнаи_баҳо()\n\nнавис(\"Журнал\")\nнавис(такрор(\"=\", 46))\n\nбарои д аз тартиб_бо(донишҷӯён, номи):\n навис(д.ном, \"|\", гирд(д.миёнаи_баҳо(), 2), \"|\", д.хулоса())\n\nнавис(такрор(\"=\", 46))\n\nбигзор беҳтарин = тартиб_бо(донишҷӯён, баҳои)[-1]\nнавис(\"Беҳтарин:\", беҳтарин.ном)\nнавис(беҳтарин.салом()) # функсия аз қолиби волид\nнавис(\"Навъи объект:\", навъ(беҳтарин))\n", "чаро_ва_қувват.tj": "# чаро, |> ва хатоҳои навъдор\n\n# --- чаро: \"чаро тағйирёбандаи ман ҳамин аст?\" ---------------------------\nбигзор ҷамъ = 0\nбарои i аз 1 то 5:\n ҷамъ += i\n\nнавис(\"Ҷамъ:\", ҷамъ)\nнавис(\"\")\nчаро(ҷамъ)\n\nнавис(\"\")\nнавис(такрор(\"-\", 46))\n\n# --- |> : амалҳо аз чап ба рост, мисли ҷумлаи тоҷикӣ ---------------------\nбигзор номҳо = [\"Яқубов\", \"Ғафуров\", \"Аҳмадов\"]\n\nномҳо |> тартиб |> навис\nнавис([3, 1, 2] |> тартиб |> баръакс)\nнавис(\"салом ҷаҳон\" |> калон)\nнавис(\"а,б,в\" |> ҷудо(\",\") |> дарозӣ)\n\nнавис(такрор(\"-\", 46))\n\n# --- хатоҳои навъдор -----------------------------------------------------\nфунксия бехатар(амал):\n кӯшиш:\n амал()\n хато \"тақсим_ба_сифр\":\n навис(\" ба сифр тақсим кардан мумкин нест\")\n хато \"берун_аз_ҳудуд\":\n навис(\" чунин унсур нест\")\n хато паём:\n навис(\" хатои дигар:\", паём)\n\nфунксия як():\n навис(10 / 0)\n\nфунксия ду():\n навис([1, 2][99])\n\nфунксия се():\n навис(не \"матн\")\n\nнавис(\"Санҷиши хатоҳо:\")\nбехатар(як)\nбехатар(ду)\nбехатар(се)\nнавис(\"Барнома то охир расид.\")\n"}, "stdin": {"саволнома.tj": "Баҳром\n16"}}; const MAX_ITERATIONS = 200000; const NL = String.fromCharCode(10); const codeEl = document.getElementById("code"); const stdinEl = document.getElementById("stdin"); const pageEl = document.getElementById("tajik-page"); const pageLabelEl = document.getElementById("page-label"); const outEl = document.getElementById("output"); const runEl = document.getElementById("run"); const pickEl = document.getElementById("examples"); for (const [name, label] of EXAMPLES.order) { const option = document.createElement("option"); option.value = name; option.textContent = label; pickEl.appendChild(option); } function loadExample(name) { codeEl.value = EXAMPLES.sources[name] || ""; stdinEl.value = EXAMPLES.stdin[name] || ""; outEl.textContent = ""; outEl.className = ""; } pickEl.addEventListener("change", () => loadExample(pickEl.value)); loadExample(EXAMPLES.order[0][0]); let pyodide = null; let runProgram = null; // Input for `хонед`. Lines typed in the input box are used first; only when // they run out do we fall back to a prompt dialog — which sandboxed frames // and some browsers refuse outright, so it can never be the only route. let inputLines = []; let inputIndex = 0; window.tajikReadLine = function (promptText) { if (inputIndex < inputLines.length) { return inputLines[inputIndex++]; } try { return window.prompt(promptText || "Маълумот ворид кунед:"); } catch (err) { return null; // prompt() unavailable — treat as end of input } }; async function boot() { pyodide = await loadPyodide({ indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.4/full/" }); pyodide.FS.mkdir("/lib/tajiklang"); for (const [name, source] of Object.entries(MODULES)) { pyodide.FS.writeFile("/lib/tajiklang/" + name, source); } runProgram = pyodide.runPython(` import sys sys.path.insert(0, "/lib") from tajiklang import Lexer, Parser, Interpreter, TajikLangError from tajiklang import interpreter as _interpreter from tajiklang.checker import check_program from js import tajikReadLine as _js_read_line # A phone must not be able to lock itself up on a runaway loop. _interpreter.MAX_ITERATIONS = ${MAX_ITERATIONS} # chr(10) rather than a backslash escape: this Python sits inside a JS # template literal inside a Python format string, and a backslash would be # eaten by whichever of those three layers got to it first. _NL = chr(10) # How хонед reads input in the browser: a JS prompt box, None if cancelled. # # No docstring and no backticks in this block. It is Python inside a JS # template literal inside a Python string: a triple quote would end the outer # string, and a backtick would end the template literal. def _read_line(text): answer = _js_read_line(text) return None if answer is None else str(answer) def _run(source): lines = [] try: lexer = Lexer(source, "<майдонча>") program = Parser(lexer.tokenize(), lexer.lines).parse() interp = Interpreter(lexer.lines, lines.append, read_line=_read_line) # Check before running: every problem at once, before any output. found = check_program(program, set(interp.builtins.values)) errors = [d for d in found if not d.is_warning] if errors: report = [] for problem in errors: report.append(problem.format(lexer.lines)) report.append("Ҳамагӣ " + str(len(errors)) + " хато. Барнома иҷро нашуд.") return ((_NL * 2).join(report), True) interp.run(program) except TajikLangError as error: return (_NL.join(lines + ["", error.format()]), True) except RecursionError: return (_NL.join(lines + ["", "Хатои иҷро: рекурсия аз ҳад чуқур шуд."]), True) except Exception as error: # A fault in the language itself. The student sees Tajik, never a # traceback: they have done nothing wrong and English filenames tell # them nothing they can act on. from tajiklang.errors import internal_report return (_NL.join(lines + ["", internal_report(error, "майдонча")]), True) return (_NL.join(lines), False) _run `); runEl.disabled = false; outEl.textContent = "Тайёр. Тугмаи «Иҷро» -ро пахш кунед."; } function run() { if (!runProgram) return; outEl.textContent = "Иҷро…"; outEl.className = ""; // Split on newlines without writing a backslash: TEMPLATE is a plain // Python string, so an escape here would become a real control // character and the regex literal would not survive the trip. const CR = String.fromCharCode(13); const LF = String.fromCharCode(10); inputLines = stdinEl.value === "" ? [] : stdinEl.value.split(CR).join("").split(LF); inputIndex = 0; // The page pane belongs to the run that drew it, so it is emptied first // and revealed only if this program actually wrote something. pageEl.innerHTML = ""; pageEl.hidden = true; pageLabelEl.hidden = true; // let the browser paint "Иҷро…" before the interpreter blocks the thread setTimeout(() => { try { const result = runProgram(codeEl.value); const [text, failed] = result.toJs(); result.destroy(); const drew = pageEl.childElementCount > 0; pageEl.hidden = !drew; pageLabelEl.hidden = !drew; outEl.textContent = text.trim() === "" ? (drew ? "(саҳифа сохта шуд)" : "(бе натиҷа)") : text; outEl.className = failed ? "error" : ""; } catch (err) { // Never show the raw error: it carries the implementation's own // wording, which means nothing to a student. outEl.textContent = "Хатои дохилии забон." + NL + NL + "Ин хатои барномаи шумо нест." + NL + "Лутфан хабар диҳед: " + "https://github.com/BahromPython/tajiklang/issues"; outEl.className = "error"; console.error(err); } }, 10); } runEl.addEventListener("click", run); document.addEventListener("keydown", (event) => { if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { event.preventDefault(); run(); } }); boot().catch((err) => { outEl.textContent = "Пайвастшавӣ нашуд: " + err + "\n\nБарои кор кардан интернет лозим аст (бори аввал)."; outEl.className = "error"; });