Add immutable revisions, PGVN editor, asset replacement, merge and rebase workflows, authenticated media delivery and reachability GC. Harden the script interpreter, pin unrpyc and remove tracked build metadata. Include current compatibility checks, Naninovel import and regression tests.
136 lines
5.4 KiB
Python
136 lines
5.4 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 == "call":
|
|
ex("f")
|
|
for item in seq("a"):
|
|
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 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": ()}
|
|
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)
|
|
|
|
|
|
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)
|