feat: improve RenPy conversion and support MIST runtime
CI / frontend (push) Failing after 15s
CI / backend (push) Failing after 37s

This commit is contained in:
mrleo1nid committed 2026-10-25 23:16:33 +03:00
1 parent f7f7f5b1b3
commit f9eb5c1212
44 files changed
+2822 -174

No files matched your search

+52 -8
View File
@@ -25,7 +25,7 @@ UNARY_OPS = {ast.Not: "not", ast.USub: "-", ast.UAdd: "+", ast.Invert: "~"}
# Modules whose import is harmless: the player provides the functions it supports.
KNOWN_MODULES = {"random", "math", "renpy", "store", "time", "datetime", "re", "copy", "collections",
"itertools", "string", "functools", "operator", "json"}
"itertools", "string", "functools", "operator", "json", "gc"}
class Unsupported(Exception):
@@ -36,25 +36,67 @@ def const(v: Any) -> Expr:
return {"k": "const", "v": v}
def python2_operators(source: str) -> str:
"""Ren'Py 7 games are Python 2: `a <> b` means `a != b`. Quoted text is left alone."""
out: list[str] = []
quote = ""
i = 0
while i < len(source):
ch = source[i]
if quote:
out.append(ch)
if ch == "\\" and i + 1 < len(source):
out.append(source[i + 1])
i += 1
elif ch == quote:
quote = ""
elif ch in "\"'":
quote = ch
out.append(ch)
elif source.startswith("<>", i):
out.append("!=")
i += 1
else:
out.append(ch)
i += 1
return "".join(out)
def parse_expr(source: str) -> Expr:
try:
tree = ast.parse(source.strip(), mode="eval")
except SyntaxError as e:
raise Unsupported(f"синтаксическая ошибка в выражении: {source!r}") from e
try:
tree = ast.parse(python2_operators(source).strip(), mode="eval")
except SyntaxError:
raise Unsupported(f"синтаксическая ошибка в выражении: {source!r}") from e
return expr(tree.body)
def parse_stmts(source: str) -> list[Stmt]:
try:
tree = ast.parse(_dedent(source))
tree = python_ast(source)
except SyntaxError as e:
raise Unsupported(f"синтаксическая ошибка в python: {e.msg} ({source[:60]!r})") from e
try:
tree = python_ast(python2_operators(source))
except SyntaxError:
raise Unsupported(f"синтаксическая ошибка в python: {e.msg} ({source[:60]!r})") from e
return stmts(tree.body)
def _dedent(source: str) -> str:
import textwrap
return textwrap.dedent(source)
def python_ast(source: str, mode: str = "exec") -> ast.AST:
"""Parse an embedded block as Ren'Py does, preserving string contents/columns.
Physical continuation lines may start at column zero. Wrapping the indented
code avoids dedenting those lines or changing whitespace inside literals.
"""
first = next((line for line in source.splitlines() if line.strip()), "")
indented = mode == "exec" and first.startswith((" ", "\t"))
tree = ast.parse("if True:\n" + source if indented else source, mode=mode)
if indented:
tree.body = tree.body[0].body
ast.increment_lineno(tree, -1)
return tree
def literal(source: str) -> Any:
@@ -164,6 +206,8 @@ def stmt(node: ast.stmt) -> list[Stmt]:
if type(op) not in BIN_OPS:
raise Unsupported(f"оператор {type(op).__name__}")
return [{"op": "aug", "t": target(t), "o": BIN_OPS[type(op)], "v": expr(value)}]
case ast.Expr(value=ast.Call(func=func)) if ast.unparse(func).startswith("build."):
return [] # distribution settings (build.classify, build.archive ...) have no meaning in the browser
case ast.Expr(value=ast.Constant()):
return [] # docstring / bare literal
case ast.Expr(value=value):
@@ -220,7 +264,7 @@ def stmt(node: ast.stmt) -> list[Stmt]:
# Methods the player can run implicitly. Any other dunder method would silently
# never be called, so the converter refuses it instead.
SUPPORTED_DUNDERS = {"__init__", "__str__", "__repr__"}
SUPPORTED_DUNDERS = {"__init__", "__str__", "__repr__", "__call__", "__eq__"}
INACTIVE_VARIANTS = {"gui.variant", "renpy.variant"}
TRANSPARENT_DECORATORS = {"renpy.pure"} # marks a function as side-effect free; no runtime meaning
DECORATORS = {"staticmethod", "classmethod", "property"}