335 lines
No EOL
12 KiB
Python
335 lines
No EOL
12 KiB
Python
from __future__ import annotations
|
||
import operator
|
||
from typing import TYPE_CHECKING, TypeAlias
|
||
if TYPE_CHECKING:
|
||
Token: TypeAlias = tuple[str, str]
|
||
|
||
import re
|
||
# === AST Node Definitions ===
|
||
|
||
_ops = {
|
||
"==": (str, operator.eq),
|
||
"!=": (str, operator.ne),
|
||
"<": (float, operator.lt),
|
||
">": (float, operator.gt),
|
||
"<=": (float, operator.le),
|
||
">=": (float, operator.ge),
|
||
}
|
||
|
||
class ExprNode:
|
||
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
|
||
raise NotImplementedError("Must implement eval in subclass")
|
||
|
||
class LiteralNode(ExprNode):
|
||
def __init__(self, value: str | re.Pattern[str]) -> None:
|
||
self.value: str | re.Pattern[str] = value
|
||
|
||
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
|
||
return self.value
|
||
|
||
class VarNode(ExprNode):
|
||
def __init__(self, name: str) -> None:
|
||
self.name: str = name
|
||
|
||
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
|
||
return context.get(self.name, '0')
|
||
|
||
|
||
class IndexNode(ExprNode):
|
||
def __init__(self, name: str, index: int) -> None:
|
||
self.name: str = name
|
||
self.index: int = index
|
||
|
||
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
|
||
val = context.get(self.name, '0')
|
||
val = val.split(',')
|
||
if self.index > len(val) -1: return '0'
|
||
return val[self.index]
|
||
|
||
|
||
class UnaryOpNode(ExprNode):
|
||
def __init__(self, op: str, child: ExprNode) -> None:
|
||
self.op = op
|
||
self.child = child
|
||
|
||
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
|
||
cval = self.child.eval(context)
|
||
if isinstance(cval, re.Pattern):
|
||
raise TypeError("Unary Operator cannot evaluate type re.Pattern")
|
||
if self.op == "!":
|
||
return not float(cval)
|
||
raise RuntimeError(f"Unknown unary operator {self.op}")
|
||
|
||
class BinaryOpNode(ExprNode):
|
||
def __init__(self, left: ExprNode, op: str, right: ExprNode) -> None:
|
||
self.left = left
|
||
self.op = op
|
||
self.right = right
|
||
|
||
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
|
||
if self.op in ["and", "&&"]:
|
||
if not self.left.eval(context): return False
|
||
return self.right.eval(context)
|
||
|
||
if self.op == "or":
|
||
if self.left.eval(context): return True
|
||
return self.right.eval(context)
|
||
|
||
# For everything else, we evaluate both sides
|
||
lv: str | float | re.Pattern[str] | bool = self.left.eval(context)
|
||
rv: str | float | re.Pattern[str] | bool = self.right.eval(context)
|
||
|
||
caster, func = _ops.get(self.op, (None, None))
|
||
|
||
if caster and func:
|
||
if isinstance(lv, re.Pattern) or isinstance(rv, re.Pattern):
|
||
raise RuntimeError(f"Operands of {self.op} must not be regex patterns")
|
||
return func(caster(lv), caster(rv))
|
||
|
||
if self.op == "=~":
|
||
if not isinstance(rv, re.Pattern):
|
||
raise TypeError("Right operand of =~ must be a regex pattern")
|
||
return rv.search(str(lv)) is not None
|
||
|
||
if self.op == "!~":
|
||
if not isinstance(rv, re.Pattern):
|
||
raise TypeError("Right operand of !~ must be a regex pattern")
|
||
return rv.search(str(lv)) is None
|
||
|
||
raise RuntimeError(f"Unknown binary operator {self.op}")
|
||
|
||
# === Parser & Tokenizer ===
|
||
|
||
class Parser:
|
||
def __init__(self, text: str):
|
||
self.text = text
|
||
self.tokens: list[Token] = self._tokenize(text)
|
||
self.i = 0 # index into tokens
|
||
|
||
def _tokenize(self, text: str) -> list[Token]:
|
||
tokens: list[Token] = []
|
||
i = 0
|
||
length: int = len(text)
|
||
|
||
while i < length:
|
||
c = text[i]
|
||
|
||
# Skip whitespace
|
||
if c.isspace():
|
||
i += 1
|
||
continue
|
||
|
||
# Parentheses / Brackets
|
||
if c == "(":
|
||
tokens.append(("LPAREN", c)); i += 1; continue
|
||
if c == ")":
|
||
tokens.append(("RPAREN", c)); i += 1; continue
|
||
if c == "[":
|
||
tokens.append(("LBRACK", c)); i += 1; continue
|
||
if c == "]":
|
||
tokens.append(("RBRACK", c)); i += 1; continue
|
||
|
||
# Multi‐char operators: check longest first
|
||
# Order matters: check 3‐character operators? We only have 2‐char: ==, !=, >=, <=, =~, !~, and the single‐char '!'
|
||
if text.startswith("==", i):
|
||
tokens.append(("OP", "==")); i += 2; continue
|
||
if text.startswith("!=", i):
|
||
tokens.append(("OP", "!=")); i += 2; continue
|
||
if text.startswith(">=", i):
|
||
tokens.append(("OP", ">=")); i += 2; continue
|
||
if text.startswith("<=", i):
|
||
tokens.append(("OP", "<=")); i += 2; continue
|
||
if text.startswith("=~", i):
|
||
tokens.append(("OP", "=~")); i += 2; continue
|
||
if text.startswith("!~", i):
|
||
tokens.append(("OP", "!~")); i += 2; continue
|
||
if text.startswith("&&", i):
|
||
tokens.append(("OP", "and")); i += 2; continue
|
||
if text.startswith("||", i):
|
||
tokens.append(("OP", "or")); i += 2; continue
|
||
|
||
# Single‐char '>' or '<'
|
||
if c == ">":
|
||
tokens.append(("OP", ">")); i += 1; continue
|
||
if c == "<":
|
||
tokens.append(("OP", "<")); i += 1; continue
|
||
|
||
# Single‐char '!'
|
||
if c == "!":
|
||
tokens.append(("OP", "!")); i += 1; continue
|
||
|
||
# Regex literal: starts with '/'
|
||
if c == "/":
|
||
# Read until the next unescaped '/'
|
||
j = i + 1
|
||
while j < length:
|
||
if text[j] == "/" and text[j-1] != "\\":
|
||
break
|
||
j += 1
|
||
if j >= length:
|
||
raise SyntaxError("Unterminated regex literal")
|
||
pattern_body = text[i+1:j]
|
||
tokens.append(("REGEX", pattern_body))
|
||
i = j + 1
|
||
continue
|
||
|
||
# Quoted string literal: " … " or ' … '
|
||
if c in ("'", '"'):
|
||
quote_char = c
|
||
j = i + 1
|
||
while j < length:
|
||
if text[j] == quote_char and text[j-1] != "\\":
|
||
break
|
||
j += 1
|
||
if j >= length:
|
||
raise SyntaxError("Unterminated string literal")
|
||
str_body = text[i+1: j]
|
||
# We do not interpret escape sequences here; we store raw content
|
||
tokens.append(("STRING", str_body))
|
||
i = j + 1
|
||
continue
|
||
|
||
# Number: [0-9]+(\.[0-9]+)?
|
||
if c.isdigit():
|
||
j = i
|
||
while j < length and (text[j].isdigit() or text[j] == "."):
|
||
j += 1
|
||
num_text = text[i:j]
|
||
tokens.append(("NUMBER", num_text))
|
||
i = j
|
||
continue
|
||
|
||
# Identifier or keyword (and/or)
|
||
if c.isalpha() or c == "_":
|
||
j = i
|
||
while j < length and (text[j].isalnum() or text[j] == "_"):
|
||
j += 1
|
||
ident = text[i:j]
|
||
if ident == "and":
|
||
tokens.append(("OP", "and"))
|
||
elif ident == "or":
|
||
tokens.append(("OP", "or"))
|
||
else:
|
||
tokens.append(("IDENT", ident))
|
||
i = j
|
||
continue
|
||
|
||
# If we get here, it's an unexpected character
|
||
raise SyntaxError(f"Unexpected character: {c}")
|
||
|
||
return tokens
|
||
|
||
def _peek(self) -> Token | None:
|
||
if self.i < len(self.tokens):
|
||
return self.tokens[self.i]
|
||
return None
|
||
|
||
def _advance(self) -> Token:
|
||
if self.i >= len(self.tokens):
|
||
raise SyntaxError("Unexpected end of input")
|
||
tok = self.tokens[self.i]
|
||
self.i += 1
|
||
return tok
|
||
|
||
def _expect(self, ttype: str, value: str | None = None) -> Token:
|
||
tok = self._peek()
|
||
if tok is None:
|
||
raise SyntaxError(f"Expected {ttype} but found end of input")
|
||
if tok[0] != ttype or (value is not None and tok[1] != value):
|
||
raise SyntaxError(f"Expected {ttype} {value!r} but found {tok}")
|
||
return self._advance()
|
||
|
||
def parse(self) -> ExprNode:
|
||
node = self._parse_or()
|
||
if self._peek() is not None:
|
||
raise SyntaxError(f"Unexpected token after end: {self._peek()}")
|
||
return node
|
||
|
||
def _parse_or(self) -> ExprNode:
|
||
left = self._parse_and()
|
||
while True:
|
||
tok = self._peek()
|
||
if tok is not None and tok[0] == "OP" and tok[1] == "or":
|
||
self._advance()
|
||
right = self._parse_and()
|
||
left = BinaryOpNode(left, "or", right)
|
||
else:
|
||
break
|
||
return left
|
||
|
||
def _parse_and(self) -> ExprNode:
|
||
left = self._parse_not()
|
||
while True:
|
||
tok = self._peek()
|
||
if tok is not None and tok[0] == "OP" and tok[1] == "and":
|
||
self._advance()
|
||
right = self._parse_not()
|
||
left = BinaryOpNode(left, "and", right)
|
||
else:
|
||
break
|
||
return left
|
||
|
||
def _parse_not(self) -> ExprNode:
|
||
tok = self._peek()
|
||
# If we see a lone '!' (not followed by '=' or '~'), it is unary NOT
|
||
if tok is not None and tok[0] == "OP" and tok[1] == "!":
|
||
# Make sure it's not part of '!=' or '!~' (those would have been tokenized as OP("!=") or OP("!~"))
|
||
self._advance()
|
||
child = self._parse_not()
|
||
return UnaryOpNode("!", child)
|
||
else:
|
||
return self._parse_comp()
|
||
|
||
def _parse_comp(self) -> ExprNode:
|
||
left = self._parse_atom()
|
||
tok = self._peek()
|
||
if tok is not None and tok[0] == "OP" and tok[1] in ("==", "!=", "<", ">", "<=", ">=", "=~", "!~"):
|
||
op = tok[1]
|
||
self._advance()
|
||
right = self._parse_atom()
|
||
return BinaryOpNode(left, op, right)
|
||
return left
|
||
|
||
def _parse_atom(self) -> ExprNode:
|
||
tok = self._peek()
|
||
if tok is None:
|
||
raise SyntaxError("Unexpected end of input in atom")
|
||
|
||
ttype, val = tok
|
||
|
||
if ttype in ["STRING", "NUMBER"]:
|
||
self._advance()
|
||
return LiteralNode(val)
|
||
|
||
if ttype == "REGEX":
|
||
self._advance()
|
||
|
||
try:
|
||
pat = re.compile(val)
|
||
except re.error as e:
|
||
raise SyntaxError(f"Invalid regex /{val}/: {e}")
|
||
|
||
return LiteralNode(pat)
|
||
|
||
if ttype == "IDENT":
|
||
# Could be a variable or keyword—but "and"/"or" handled earlier
|
||
name = val
|
||
self._advance()
|
||
# Check for indexing: IDENT '[' NUMBER ']'
|
||
nxt = self._peek()
|
||
if nxt is not None and nxt[0] == "LBRACK":
|
||
self._advance() # consume '['
|
||
idx_tok = self._expect("NUMBER")
|
||
idx = int(idx_tok[1])
|
||
self._expect("RBRACK")
|
||
return IndexNode(name, idx)
|
||
else:
|
||
return VarNode(name)
|
||
|
||
if ttype == "LPAREN":
|
||
self._advance()
|
||
node = self._parse_or()
|
||
self._expect("RPAREN")
|
||
return node
|
||
|
||
raise SyntaxError(f"Unexpected token in atom: {tok}") |