135 lines
6.1 KiB
Python
135 lines
6.1 KiB
Python
"""Parse Naninovel expressions into data, never Python/JavaScript source code."""
|
|
import re
|
|
import math
|
|
|
|
TOKEN = re.compile(r'''\s*(?:(\d+(?:\.\d*)?(?:[eE][+-]?\d+)?|\.\d+)|([A-Za-z_]\w*)|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|(&&|\|\||==|!=|<>|<=|>=|<<|>>|[()+\-*/%<>=!~?:,&|^]))''')
|
|
PRECEDENCE = {"or": 2, "||": 2, "and": 3, "&&": 3, "|": 4, "^": 5,
|
|
"&": 6, "==": 7, "=": 7, "!=": 7, "<>": 7, "<": 8, ">": 8,
|
|
"<=": 8, ">=": 8, "<<": 9, ">>": 9, "+": 10, "-": 10,
|
|
"*": 11, "/": 11, "%": 11}
|
|
FUNCTIONS = {"Random", "random", "Rand", "GetRandValue", "Min", "Max", "Abs", "Round", "Pow"}
|
|
ARITY = {"Random": (1, 2), "random": (1, 2), "Rand": (1, 2), "GetRandValue": (1, 1),
|
|
"Min": (1, 100), "Max": (1, 100), "Abs": (1, 1), "Round": (1, 2), "Pow": (2, 2)}
|
|
|
|
|
|
def parse(source: str, functions: set[str] | None = None) -> list:
|
|
source = source.strip().removesuffix(";").strip()
|
|
tokens, offset = [], 0
|
|
while offset < len(source):
|
|
if source[offset].isspace():
|
|
offset += 1
|
|
continue
|
|
wrapped = source[offset] == "\\" and offset + 1 < len(source) and source[offset + 1] in "\"'"
|
|
if source[offset] in "\"'" or wrapped:
|
|
quote = source[offset + 1] if wrapped else source[offset]
|
|
offset += 2 if wrapped else 1
|
|
value, closed = [], False
|
|
while offset < len(source):
|
|
char = source[offset]
|
|
if wrapped and char == "\\" and offset + 1 < len(source) and source[offset + 1] == quote:
|
|
offset += 2
|
|
closed = True
|
|
break
|
|
if not wrapped and char == quote:
|
|
offset += 1
|
|
closed = True
|
|
break
|
|
if char == "\\" and offset + 1 < len(source):
|
|
offset += 1
|
|
char = {"n": "\n", "r": "\r", "t": "\t"}.get(source[offset], source[offset])
|
|
value.append(char)
|
|
offset += 1
|
|
if not closed:
|
|
raise ValueError("Не закрыта строка в выражении Naninovel")
|
|
tokens.append((2, "".join(value)))
|
|
continue
|
|
match = TOKEN.match(source, offset)
|
|
if not match:
|
|
raise ValueError(f"Не поддерживается выражение Naninovel: {source[offset:offset + 60]}")
|
|
tokens.append((next(i for i, v in enumerate(match.groups()) if v is not None),
|
|
next(v for v in match.groups() if v is not None)))
|
|
offset = match.end()
|
|
position = 0
|
|
allowed = FUNCTIONS if functions is None else functions
|
|
|
|
def peek():
|
|
return ("\x00string" if tokens[position][0] == 2 else tokens[position][1]) if position < len(tokens) else ""
|
|
|
|
def take(expected=None):
|
|
nonlocal position
|
|
if position >= len(tokens) or (expected is not None and peek() != expected):
|
|
raise ValueError(f"Ожидалось {expected or 'выражение'}: {source}")
|
|
token = tokens[position]
|
|
position += 1
|
|
return token
|
|
|
|
def expression(minimum=1):
|
|
kind, token = take()
|
|
if kind != 2 and token in {"-", "+", "!", "not", "~"}:
|
|
left = ["unary", token, expression(12)]
|
|
elif kind == 3 and token == "(":
|
|
left = expression()
|
|
take(")")
|
|
elif kind == 0:
|
|
value = float(token) if any(c in token for c in ".eE") else int(token)
|
|
if not math.isfinite(value):
|
|
raise ValueError("Неконечная числовая константа Naninovel")
|
|
left = ["const", value]
|
|
elif kind == 2:
|
|
left = ["const", token]
|
|
elif kind == 1:
|
|
if token.lower() in {"true", "false", "null"}:
|
|
left = ["const", {"true": True, "false": False, "null": None}[token.lower()]]
|
|
elif peek() == "(":
|
|
if token not in allowed and token.lower() != "if":
|
|
raise ValueError(f"Не поддерживается функция Naninovel: {token}")
|
|
take("(")
|
|
args = []
|
|
if peek() != ")":
|
|
while True:
|
|
args.append(expression())
|
|
if peek() != ",":
|
|
break
|
|
take(",")
|
|
take(")")
|
|
if functions is None and token in ARITY and not ARITY[token][0] <= len(args) <= ARITY[token][1]:
|
|
raise ValueError(f"Неподдерживаемое число аргументов Naninovel: {token}")
|
|
if token.lower() == "if":
|
|
if len(args) != 3:
|
|
raise ValueError("If Naninovel требует три аргумента")
|
|
left = ["if", *args]
|
|
else:
|
|
left = ["call", token, args]
|
|
else:
|
|
left = ["name", token]
|
|
else:
|
|
raise ValueError(f"Неожиданный токен {token}: {source}")
|
|
while peek() in PRECEDENCE and PRECEDENCE[peek()] >= minimum:
|
|
op = take()[1]
|
|
left = ["bin", op, left, expression(PRECEDENCE[op] + 1)]
|
|
if minimum <= 1 and peek() == "?":
|
|
take("?")
|
|
yes = expression()
|
|
take(":")
|
|
left = ["if", left, yes, expression()]
|
|
return left
|
|
|
|
ast = expression()
|
|
if position != len(tokens):
|
|
raise ValueError(f"Лишние токены в выражении Naninovel: {source}")
|
|
return ast
|
|
|
|
|
|
def expr(source: str, functions: set[str] | None = None) -> dict:
|
|
return {"k": "nani", "ast": parse(source, functions)}
|
|
|
|
|
|
def assignment(source: str, functions: set[str] | None = None) -> dict:
|
|
match = re.fullmatch(r"\s*([A-Za-z_]\w*)\s*([+\-*/%&|^]?)=(?!=)(.*?)\s*;?\s*", source, re.S)
|
|
if not match:
|
|
raise ValueError(f"Не поддерживается присваивание Naninovel: {source}")
|
|
ast = parse(match[3], functions)
|
|
if match[2]:
|
|
ast = ["bin", match[2], ["name", match[1]], ast]
|
|
return {"op": "set", "t": {"k": "name", "id": match[1]}, "v": {"k": "nani", "ast": ast}}
|