- 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>
172 lines
7.1 KiB
Python
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)
|