nodeslicer/infra/prusaslicer_profiles/parser.py
2026-08-25 20:47:31 +03:00

291 lines
10 KiB
Python

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