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}')