feat: add native Tomie adapter and strict import compatibility checks
This commit is contained in:
1 parent
c8d83b3fe7
commit
57a4edcbfd
45 files changed
+2985
-71
No files matched your search
@@ -1,5 +1,6 @@
|
||||
"""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,
|
||||
@@ -7,12 +8,41 @@ PRECEDENCE = {"or": 2, "||": 2, "and": 3, "&&": 3, "|": 4, "^": 5,
|
||||
"<=": 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]}")
|
||||
@@ -23,7 +53,7 @@ def parse(source: str, functions: set[str] | None = None) -> list:
|
||||
allowed = FUNCTIONS if functions is None else functions
|
||||
|
||||
def peek():
|
||||
return tokens[position][1] if position < len(tokens) else ""
|
||||
return ("\x00string" if tokens[position][0] == 2 else tokens[position][1]) if position < len(tokens) else ""
|
||||
|
||||
def take(expected=None):
|
||||
nonlocal position
|
||||
@@ -35,22 +65,23 @@ def parse(source: str, functions: set[str] | None = None) -> list:
|
||||
|
||||
def expression(minimum=1):
|
||||
kind, token = take()
|
||||
if token in {"-", "+", "!", "not", "~"}:
|
||||
if kind != 2 and token in {"-", "+", "!", "not", "~"}:
|
||||
left = ["unary", token, expression(12)]
|
||||
elif token == "(":
|
||||
elif kind == 3 and token == "(":
|
||||
left = expression()
|
||||
take(")")
|
||||
elif kind == 0:
|
||||
left = ["const", float(token) if any(c in token for c in ".eE") else int(token)]
|
||||
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:
|
||||
# Decode only conventional escapes; preserve all Unicode prose.
|
||||
left = ["const", re.sub(r"\\([\\\"'nrt])", lambda m:
|
||||
{"n": "\n", "r": "\r", "t": "\t"}.get(m[1], m[1]), token[1:-1])]
|
||||
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:
|
||||
if token not in allowed and token.lower() != "if":
|
||||
raise ValueError(f"Не поддерживается функция Naninovel: {token}")
|
||||
take("(")
|
||||
args = []
|
||||
@@ -61,7 +92,14 @@ def parse(source: str, functions: set[str] | None = None) -> list:
|
||||
break
|
||||
take(",")
|
||||
take(")")
|
||||
left = ["call", token, args]
|
||||
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:
|
||||
@@ -82,12 +120,15 @@ def parse(source: str, functions: set[str] | None = None) -> list:
|
||||
return ast
|
||||
|
||||
|
||||
def expr(source: str) -> dict:
|
||||
return {"k": "nani", "ast": parse(source)}
|
||||
def expr(source: str, functions: set[str] | None = None) -> dict:
|
||||
return {"k": "nani", "ast": parse(source, functions)}
|
||||
|
||||
|
||||
def assignment(source: str) -> dict:
|
||||
match = re.fullmatch(r"\s*([A-Za-z_]\w*)\s*=(?!=)(.*?)\s*;?\s*", source, re.S)
|
||||
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}")
|
||||
return {"op": "set", "t": {"k": "name", "id": match[1]}, "v": expr(match[2])}
|
||||
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}}
|
||||
Reference in new issue
Block a user