159 lines
5.7 KiB
Python
159 lines
5.7 KiB
Python
from __future__ import annotations
|
|
import operator
|
|
import re
|
|
from typing import Any
|
|
|
|
TOKENS = [
|
|
('OP', r'==|!=|<=|>=|=~|!~|&&|\|\||[()\[\]!><]'),
|
|
('REGEX', r'/((?:\\/|[^/])*)/'),
|
|
('STRING', r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\''),
|
|
('NUMBER', r'\d+(?:\.\d+)?'),
|
|
('IDENT', r'[a-zA-Z_]\w*'),
|
|
]
|
|
TOKEN_RE = re.compile('|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKENS))
|
|
|
|
_OPS = {
|
|
'==': (str, operator.eq),
|
|
'!=': (str, operator.ne),
|
|
'<': (float, operator.lt),
|
|
'>': (float, operator.gt),
|
|
'<=': (float, operator.le),
|
|
'>=': (float, operator.ge),
|
|
}
|
|
|
|
def _tokenize(text: str) -> list[tuple[str, Any]]:
|
|
tokens: list[tuple[str, Any]] = []
|
|
for m in TOKEN_RE.finditer(text):
|
|
kind: str = m.lastgroup if m.lastgroup else ""
|
|
val = m.group(kind)
|
|
if kind == 'OP':
|
|
if val == '&&': tokens.append(('OP', 'and'))
|
|
elif val == '||': tokens.append(('OP', 'or'))
|
|
elif val in ('(', ')', '[', ']'): tokens.append((val, val))
|
|
else: tokens.append(('OP', val))
|
|
elif kind == 'REGEX':
|
|
tokens.append(('REGEX', re.compile(val[1:-1])))
|
|
elif kind == 'STRING':
|
|
tokens.append(('LIT', val[1:-1]))
|
|
elif kind == 'NUMBER':
|
|
tokens.append(('LIT', val))
|
|
elif kind == 'IDENT':
|
|
if val == 'and': tokens.append(('OP', 'and'))
|
|
elif val == 'or': tokens.append(('OP', 'or'))
|
|
else: tokens.append(('IDENT', val))
|
|
return tokens
|
|
|
|
class ExprNode:
|
|
"""Represents an expression node in the AST."""
|
|
def __init__(self, node: tuple[Any, ...]) -> None:
|
|
self.node = node
|
|
|
|
def eval(self, context: dict[str, Any]) -> Any:
|
|
return self._eval_node(self.node, context)
|
|
|
|
@staticmethod
|
|
def _eval_node(node: tuple[Any, ...], context: dict[str, Any]) -> Any:
|
|
ntype = node[0]
|
|
if ntype == 'lit':
|
|
return node[1]
|
|
if ntype == 'var':
|
|
return context.get(node[1], '0')
|
|
if ntype == 'index':
|
|
val = str(context.get(node[1], '0'))
|
|
parts = val.split(',')
|
|
idx = node[2]
|
|
return parts[idx].strip() if idx < len(parts) else '0'
|
|
if ntype == 'unary':
|
|
cval = ExprNode._eval_node(node[2], context)
|
|
try:
|
|
return not float(cval)
|
|
except (ValueError, TypeError):
|
|
return not bool(cval)
|
|
if ntype == 'bin':
|
|
op, left, right = node[1], node[2], node[3]
|
|
if op == 'and':
|
|
lv = ExprNode._eval_node(left, context)
|
|
if not lv or lv == '0': return False
|
|
return ExprNode._eval_node(right, context)
|
|
if op == 'or':
|
|
lv = ExprNode._eval_node(left, context)
|
|
if lv and lv != '0': return True
|
|
return ExprNode._eval_node(right, context)
|
|
lv = ExprNode._eval_node(left, context)
|
|
rv = ExprNode._eval_node(right, context)
|
|
if op in _OPS:
|
|
caster, func = _OPS[op]
|
|
try:
|
|
return func(caster(lv), caster(rv))
|
|
except (ValueError, TypeError):
|
|
return func(str(lv), str(rv))
|
|
if op == '=~':
|
|
return rv.search(str(lv)) is not None
|
|
if op == '!~':
|
|
return rv.search(str(lv)) is None
|
|
return '0'
|
|
|
|
class Parser:
|
|
"""Recursive descent parser for PrusaSlicer expressions."""
|
|
def __init__(self, text: str) -> None:
|
|
self.tokens = _tokenize(text)
|
|
self.pos = 0
|
|
|
|
def parse(self) -> ExprNode:
|
|
node = self._parse_or()
|
|
if self.pos < len(self.tokens):
|
|
raise SyntaxError(f'Trailing tokens: {self.tokens[self.pos:]}')
|
|
return ExprNode(node)
|
|
|
|
def _peek(self) -> tuple[str, Any] | None:
|
|
return self.tokens[self.pos] if self.pos < len(self.tokens) else None
|
|
|
|
def _advance(self) -> tuple[str, Any]:
|
|
tok = self.tokens[self.pos]
|
|
self.pos += 1
|
|
return tok
|
|
|
|
def _parse_or(self) -> tuple[Any, ...]:
|
|
left = self._parse_and()
|
|
while (tok := self._peek()) and tok == ('OP', 'or'):
|
|
self._advance()
|
|
left = ('bin', 'or', left, self._parse_and())
|
|
return left
|
|
|
|
def _parse_and(self) -> tuple[Any, ...]:
|
|
left = self._parse_not()
|
|
while (tok := self._peek()) and tok == ('OP', 'and'):
|
|
self._advance()
|
|
left = ('bin', 'and', left, self._parse_not())
|
|
return left
|
|
|
|
def _parse_not(self) -> tuple[Any, ...]:
|
|
if (tok := self._peek()) and tok == ('OP', '!'):
|
|
self._advance()
|
|
return ('unary', '!', self._parse_not())
|
|
return self._parse_comp()
|
|
|
|
def _parse_comp(self) -> tuple[Any, ...]:
|
|
left = self._parse_atom()
|
|
if (tok := self._peek()) and tok[0] == 'OP' and tok[1] in ('==', '!=', '<', '>', '<=', '>=', '=~', '!~'):
|
|
op = self._advance()[1]
|
|
left = ('bin', op, left, self._parse_atom())
|
|
return left
|
|
|
|
def _parse_atom(self) -> tuple[Any, ...]:
|
|
tok = self._advance()
|
|
ttype, val = tok
|
|
if ttype in ('LIT', 'REGEX'):
|
|
return ('lit', val)
|
|
if ttype == 'IDENT':
|
|
if self._peek() == ('[', '['):
|
|
self._advance()
|
|
idx_tok = self._advance()
|
|
if self._advance() != (']', ']'): raise SyntaxError('Expected ]')
|
|
return ('index', val, int(idx_tok[1]))
|
|
return ('var', val)
|
|
if ttype == '(':
|
|
node = self._parse_or()
|
|
if self._advance() != (')', ')'): raise SyntaxError('Expected )')
|
|
return node
|
|
raise SyntaxError(f'Unexpected token: {tok}')
|