from __future__ import annotations import operator import re from typing import TYPE_CHECKING, TypeAlias, Any, cast if TYPE_CHECKING: Token: TypeAlias = tuple[str, str] # === AST Node Definitions === _ops = { "==": (str, operator.eq), "!=": (str, operator.ne), "<": (float, operator.lt), ">": (float, operator.gt), "<=": (float, operator.le), ">=": (float, operator.ge), } class ExprNode: """Base class for all expression nodes in the AST.""" def eval(self, context: dict[str, Any]) -> Any: raise NotImplementedError("Must implement eval in subclass") class LiteralNode(ExprNode): """Represents a literal value (string, number, or regex pattern).""" def __init__(self, value: Any) -> None: self.value = value def eval(self, context: dict[str, Any]) -> Any: return self.value class VarNode(ExprNode): """Represents a variable look-up in the configuration context.""" def __init__(self, name: str) -> None: self.name = name def eval(self, context: dict[str, Any]) -> Any: return context.get(self.name, '0') class IndexNode(ExprNode): """Represents an indexed variable look-up (e.g., nozzle_diameter[0]).""" def __init__(self, name: str, index: int) -> None: self.name = name self.index = index def eval(self, context: dict[str, Any]) -> Any: val = str(context.get(self.name, '0')) parts = val.split(',') if self.index >= len(parts): return '0' return parts[self.index].strip() class UnaryOpNode(ExprNode): """Represents a unary operator (e.g., !).""" def __init__(self, op: str, child: ExprNode) -> None: self.op = op self.child = child def eval(self, context: dict[str, Any]) -> Any: cval = self.child.eval(context) if isinstance(cval, re.Pattern): raise TypeError("Unary operator cannot be applied to a regex pattern.") if self.op == "!": try: return not float(cval) except (ValueError, TypeError): return not bool(cval) raise RuntimeError(f"Unknown unary operator: {self.op}") class BinaryOpNode(ExprNode): """Represents a binary operator (e.g., ==, &&, =~).""" 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, Any]) -> Any: # Short-circuiting for logical operators if self.op in ("and", "&&"): lv = self.left.eval(context) if not lv or lv == '0': return False return self.right.eval(context) if self.op in ("or", "||"): lv = self.left.eval(context) if lv and lv != '0': return True return self.right.eval(context) lv = self.left.eval(context) rv = self.right.eval(context) # Standard comparisons if self.op in _ops: caster, func = _ops[self.op] if isinstance(lv, re.Pattern) or isinstance(rv, re.Pattern): raise RuntimeError(f"Operands of {self.op} cannot be regex patterns.") try: return func(caster(lv), caster(rv)) except (ValueError, TypeError): # Fallback to string comparison if cast fails return func(str(lv), str(rv)) # Regex matching if self.op == "=~": if not isinstance(rv, re.Pattern): raise TypeError("Right operand of =~ must be a regex pattern.") pattern_str = cast(re.Pattern[str], rv) return pattern_str.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.") pattern_str = cast(re.Pattern[str], rv) return pattern_str.search(str(lv)) is None raise RuntimeError(f"Unknown binary operator: {self.op}") class Parser: """Recursive descent parser for PrusaSlicer expressions.""" def __init__(self, text: str): self.text = text self.tokens = self._tokenize(text) self.pos = 0 def _tokenize(self, text: str) -> list[Token]: tokens: list[Token] = [] i = 0 length = len(text) while i < length: c = text[i] if c.isspace(): i += 1 continue # Fixed tokens if c == "(": tokens.append(("LPAREN", "(")); i += 1; continue if c == ")": tokens.append(("RPAREN", ")")); i += 1; continue if c == "[": tokens.append(("LBRACK", "[")); i += 1; continue if c == "]": tokens.append(("RBRACK", "]")); i += 1; continue # Multi-character operators 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 if c == ">": tokens.append(("OP", ">")); i += 1; continue if c == "<": tokens.append(("OP", "<")); i += 1; continue if c == "!": tokens.append(("OP", "!")); i += 1; continue # Regex literal if c == "/": j = i + 1 while j < length: if text[j] == "/" and text[j-1] != "\\": break j += 1 if j >= length: raise SyntaxError("Unterminated regex literal.") tokens.append(("REGEX", text[i+1:j])) i = j + 1 continue # String literal if c in ("'", '"'): quote = c j = i + 1 while j < length: if text[j] == quote and text[j-1] != "\\": break j += 1 if j >= length: raise SyntaxError("Unterminated string literal.") tokens.append(("STRING", text[i+1:j])) i = j + 1 continue # Number if c.isdigit(): j = i while j < length and (text[j].isdigit() or text[j] == "."): j += 1 tokens.append(("NUMBER", text[i:j])) i = j continue # Identifier or Logical keyword 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 raise SyntaxError(f"Unexpected character in expression: {c}") return tokens def _peek(self) -> Token | None: return self.tokens[self.pos] if self.pos < len(self.tokens) else None def _advance(self) -> Token: tok = self._peek() if not tok: raise SyntaxError("Unexpected end of input.") self.pos += 1 return tok def _expect(self, ttype: str, value: str | None = None) -> Token: tok = self._peek() if not tok or tok[0] != ttype or (value and tok[1] != value): raise SyntaxError(f"Expected {ttype} {value or ''}, got {tok}") return self._advance() def parse(self) -> ExprNode: node = self._parse_or() if self._peek(): raise SyntaxError(f"Trailing tokens after expression: {self._peek()}") return node def _parse_or(self) -> ExprNode: left = self._parse_and() while (tok := self._peek()) and tok[0] == "OP" and tok[1] == "or": self._advance() left = BinaryOpNode(left, "or", self._parse_and()) return left def _parse_and(self) -> ExprNode: left = self._parse_not() while (tok := self._peek()) and tok[0] == "OP" and tok[1] == "and": self._advance() left = BinaryOpNode(left, "and", self._parse_not()) return left def _parse_not(self) -> ExprNode: if (tok := self._peek()) and tok[0] == "OP" and tok[1] == "!": self._advance() return UnaryOpNode("!", self._parse_not()) return self._parse_comp() def _parse_comp(self) -> ExprNode: left = self._parse_atom() ops = ("==", "!=", "<", ">", "<=", ">=", "=~", "!~") if (tok := self._peek()) and tok[0] == "OP" and tok[1] in ops: op = tok[1] self._advance() left = BinaryOpNode(left, op, self._parse_atom()) return left def _parse_atom(self) -> ExprNode: tok = self._advance() ttype, val = tok if ttype in ("STRING", "NUMBER"): return LiteralNode(val) if ttype == "REGEX": try: return LiteralNode(re.compile(val)) except re.error as e: raise SyntaxError(f"Invalid regex /{val}/: {e}") if ttype == "IDENT": if (nxt := self._peek()) and nxt[0] == "LBRACK": self._advance() idx = int(self._expect("NUMBER")[1]) self._expect("RBRACK") return IndexNode(val, idx) return VarNode(val) if ttype == "LPAREN": node = self._parse_or() self._expect("RPAREN") return node raise SyntaxError(f"Unexpected token in atom: {tok}")