Files
pgames-manager/backend/app/pgvn.py
T
mrleo1nidandClaude Sonnet 5.5 962595c925 feat: support user-defined functions and classes in Ren'Py python
- Converter: def/return/global/nonlocal, class (inheritance, property,
  staticmethod, classmethod), lambda, *args/**kwargs and call-site unpacking.
  Unsupported dunder methods, generators and unknown decorators are still refused
- Player: closures, super(), instances that survive save/load, isinstance,
  hasattr/getattr/setattr, map/filter/sorted(key=), recursion limit
- audit_ir accepts calls to names, classes and methods the game defines itself;
  unknown renpy.* calls still block the import. Volume/pan/pause/replay calls
  are accepted as presentation-only no-ops; build.* settings are skipped
- Tests: IR fixture compiled from tests/fixtures/classes.py (tests/make_ir.py)
  and run in the interpreter; converter tests updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 12:01:09 +03:00

172 lines
7.1 KiB
Python

"""Validation of the expression/statement AST accepted by the web interpreter."""
def expression(value, depth=0):
if depth > 64 or not isinstance(value, dict):
raise ValueError("Некорректное PGVN-выражение")
kind = value.get("k")
def ex(key, optional=False):
v = value.get(key)
if not optional or v is not None:
expression(v, depth + 1)
def seq(key):
v = value.get(key)
if not isinstance(v, list):
raise ValueError(f"Выражение {kind}: нужен список {key}")
return v
if kind == "nani":
nani_expression(value.get("ast"), depth + 1)
elif kind == "const":
if "v" not in value:
raise ValueError("const без значения")
elif kind == "name":
if not isinstance(value.get("id"), str):
raise ValueError("Имя переменной должно быть строкой")
elif kind == "attr":
ex("o")
if not isinstance(value.get("a"), str):
raise ValueError("Атрибут должен быть строкой")
elif kind in ("sub", "slice"):
ex("o")
if kind == "sub":
ex("i")
else:
for key in ("lo", "hi", "st"):
ex(key, True)
elif kind in ("bin", "unary", "bool"):
from app.converter.pyexpr import BIN_OPS, UNARY_OPS
allowed = set(BIN_OPS.values()) if kind == "bin" else set(UNARY_OPS.values()) if kind == "unary" else {"and", "or"}
if value.get("op") not in allowed:
raise ValueError("Неизвестная операция выражения")
if kind == "bin":
ex("l")
ex("r")
elif kind == "unary":
ex("v")
else:
for item in seq("v"):
expression(item, depth + 1)
elif kind == "cmp":
from app.converter.pyexpr import CMP_OPS
ex("l")
operators, operands = seq("ops"), seq("r")
if len(operators) != len(operands) or any(op not in CMP_OPS.values() for op in operators):
raise ValueError("Некорректное сравнение")
for item in operands:
expression(item, depth + 1)
elif kind == "if":
for key in ("c", "t", "e"):
ex(key)
elif kind in ("list", "tuple", "fmt", "dict"):
for item in seq("v"):
if kind == "dict":
if not isinstance(item, list) or len(item) != 2:
raise ValueError("Некорректная запись словаря")
for part in item:
expression(part, depth + 1)
elif kind != "fmt" or not isinstance(item, str):
expression(item, depth + 1)
elif kind == "star":
ex("v")
elif kind == "lambda":
parameters(value.get("params"), depth + 1)
ex("body")
elif kind == "call":
ex("f")
for item in seq("a"):
expression(item, depth + 1)
for item in value.get("ks", []):
expression(item, depth + 1)
if not isinstance(value.get("kw"), dict):
raise ValueError("Некорректные аргументы вызова")
for item in value["kw"].values():
expression(item, depth + 1)
elif kind == "comp":
for key in ("e", "t", "it"):
ex(key)
for item in seq("if"):
expression(item, depth + 1)
else:
raise ValueError(f"Неизвестное выражение: {kind}")
def parameters(params, depth):
if not isinstance(params, list) or len(params) > 64:
raise ValueError("Некорректные параметры функции")
for p in params:
if not isinstance(p, dict) or not isinstance(p.get("n"), str) or p.get("kind") not in ("p", "k", "*", "**"):
raise ValueError("Некорректный параметр функции")
if p.get("d") is not None:
expression(p["d"], depth + 1)
def statements(code, depth=0):
if depth > 64 or not isinstance(code, list):
raise ValueError("Некорректный PGVN AST")
for s in code:
if not isinstance(s, dict):
raise ValueError("Некорректный оператор AST")
op = s.get("op")
keys = {"set": ("t", "v"), "aug": ("t", "v"), "eval": ("e",),
"if": ("c",), "while": ("c",), "for": ("t", "it"),
"del": ("t",), "break": (), "continue": (), "def": (), "class": (),
"return": (), "global": (), "nonlocal": ()}
if op not in keys:
raise ValueError(f"Неизвестный оператор AST: {op}")
for key in keys[op]:
expression(s.get(key), depth + 1)
if op == "if":
statements(s.get("then"), depth + 1)
statements(s.get("else"), depth + 1)
if op in ("while", "for"):
statements(s.get("body"), depth + 1)
if op == "def":
if not isinstance(s.get("name"), str):
raise ValueError("У функции нет имени")
parameters(s.get("params"), depth + 1)
statements(s.get("body"), depth + 1)
for item in s.get("decos", []):
expression(item, depth + 1)
if op == "class":
if not isinstance(s.get("name"), str):
raise ValueError("У класса нет имени")
for item in s.get("bases", []):
expression(item, depth + 1)
statements(s.get("body"), depth + 1)
if op == "return" and s.get("v") is not None:
expression(s["v"], depth + 1)
if op in ("global", "nonlocal") and not (
isinstance(s.get("names"), list) and all(isinstance(n, str) for n in s["names"])):
raise ValueError("Некорректный список имён")
def nani_expression(node, depth=0):
"""Validate the optional Naninovel AST already supported by the shared player."""
if depth > 64 or not isinstance(node, list) or not node:
raise ValueError("Некорректный AST Naninovel")
kind = node[0]
if kind == "const" and len(node) == 2 and (node[1] is None or isinstance(node[1], (str, int, float, bool))):
return
if kind == "name" and len(node) == 2 and isinstance(node[1], str):
return
children = None
if kind == "unary" and len(node) == 3 and node[1] in ("!", "not", "+", "-", "~"):
children = node[2:]
elif kind == "bin" and len(node) == 4:
from app.converter.nani_expr import PRECEDENCE
if node[1] in PRECEDENCE:
children = node[2:]
elif kind == "if" and len(node) == 4:
children = node[1:]
elif kind == "call" and len(node) == 3:
from app.converter.nani_expr import FUNCTIONS
if node[1] in FUNCTIONS and isinstance(node[2], list):
children = node[2]
if children is None:
raise ValueError("Неизвестная операция AST Naninovel")
for child in children:
nani_expression(child, depth + 1)