"""
Expression handling utilities for mathematical expressions and evaluation.
This module defines an expression system used by the DSL layer to model, evaluate,
and serialize mathematical expressions. Expressions are represented as an object
tree with support for literals, variables, unary/binary/conditional operators,
and unary mathematical functions. Each expression can be evaluated by supplying
variable values, converted into DSL AST nodes, and inspected for variable usage.
The module contains the following public components:
* :class:`Expr` - Abstract base class for all expressions.
* :class:`Integer` - Integer literal expression.
* :class:`Float` - Floating-point literal expression with constant recognition.
* :class:`Boolean` - Boolean literal expression.
* :class:`Op` - Base class for operator expressions.
* :class:`UnaryOp` - Unary operator expression.
* :class:`BinaryOp` - Binary operator expression.
* :class:`ConditionalOp` - Ternary conditional expression.
* :class:`UFunc` - Unary mathematical function expression.
* :class:`Variable` - Variable reference expression.
* :func:`parse_expr_node_to_expr` - Convert DSL AST nodes to expression objects.
* :func:`parse_expr_from_string` - Parse DSL expression strings to expression objects.
* :func:`parse_expr` - Unified parser supporting multiple input types.
.. note::
Operator precedence is respected when converting to AST nodes. Parentheses
are inserted automatically to preserve evaluation order.
Example::
>>> from pyfcstm.model.expr import Variable, Integer, BinaryOp, UFunc
>>> expr = BinaryOp(x=Variable("x"), op="+", y=Integer(2))
>>> expr(x=3)
5
>>> func_expr = UFunc(func="sqrt", x=Integer(9))
>>> func_expr()
3.0
>>> # Parse expressions from DSL strings
>>> from pyfcstm.model.expr import parse_expr_from_string
>>> expr = parse_expr_from_string("x * 2 + 3", mode='numeric')
>>> expr(x=5)
13
>>> # Unified parsing with parse_expr
>>> from pyfcstm.model.expr import parse_expr
>>> expr = parse_expr("x + 5") # from string
>>> expr = parse_expr(expr) # from Expr object (returns directly)
"""
import math
import operator
import warnings
from dataclasses import dataclass
from typing import Iterator, List, Any
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
from .base import AstExportable
from ..dsl import node as dsl_nodes
__all__ = [
'Expr',
'Integer',
'Float',
'Boolean',
'Op',
'UnaryOp',
'BinaryOp',
'ConditionalOp',
'UFunc',
'Variable',
'parse_expr_node_to_expr',
'parse_expr_from_string',
'parse_expr',
]
[docs]
@dataclass
class Expr(AstExportable):
"""
Base class for all expressions.
This abstract class defines the common interface for all expression types.
It provides methods for traversing the expression tree, evaluating expressions,
and converting expressions to AST nodes. The class supports operator overloading
for building complex expressions using natural Python syntax.
**Operator Support:**
The Expr class provides comprehensive operator overloading for building DSL
expressions using Python syntax. All operators automatically convert their
operands using :func:`parse_expr`, supporting Expr objects, Python literals
(bool, int, float), DSL strings, and DSL AST nodes.
**Arithmetic Operators:**
- ``expr + other`` - Addition
- ``expr - other`` - Subtraction
- ``expr * other`` - Multiplication
- ``expr / other`` - Division
- ``expr % other`` - Modulo
- ``expr ** other`` - Exponentiation
- ``-expr`` - Unary negation
- ``+expr`` - Unary positive
**Bitwise Operators:**
- ``expr << other`` - Left shift
- ``expr >> other`` - Right shift
- ``expr & other`` - Bitwise AND
- ``expr | other`` - Bitwise OR
- ``expr ^ other`` - Bitwise XOR
**Comparison Operators:**
- ``expr < other`` - Less than
- ``expr <= other`` - Less than or equal
- ``expr > other`` - Greater than
- ``expr >= other`` - Greater than or equal
- ``expr.eq(other)`` - Equality (==) - method form to avoid conflict with Python's object equality
- ``expr.ne(other)`` - Not equal (!=) - method form to avoid conflict with Python's object inequality
**Logical Operators:**
- ``expr.and_(other)`` - Logical AND (&&)
- ``expr.or_(other)`` - Logical OR (||)
- ``expr.not_()`` - Logical NOT (!)
Note: Python's ``and``/``or`` keywords cannot be overloaded, and ``&``/``|``
operators are reserved for bitwise operations. Use the method forms instead.
**Conditional Operator:**
- ``expr.select(true_value, false_value)`` - Ternary operator (? :)
- ``expr.if_then_else(true_value, false_value)`` - Alias for select()
**Reverse Operators:**
All binary operators support reverse operations (e.g., ``5 + expr``) through
corresponding ``__radd__``, ``__rsub__``, etc. magic methods.
:rtype: Expr
Example::
>>> from pyfcstm.model.expr import Variable
>>> x = Variable("x")
>>> y = Variable("y")
>>>
>>> # Arithmetic operations
>>> expr = x * 2 + y
>>> expr(x=3, y=4)
10
>>>
>>> # Comparison and logical operations
>>> expr = (x > 0).and_(y > 0)
>>> expr(x=5, y=10)
True
>>>
>>> # Conditional expression
>>> expr = (x > 0).select(x, -x) # abs(x)
>>> expr(x=-5)
5
>>>
>>> # Complex nested expression
>>> expr = (x > y).select(x, y) # max(x, y)
>>> expr(x=10, y=5)
10
"""
def _iter_subs(self) -> Iterator['Expr']:
"""
Iterate over direct sub-expressions of this expression.
Subclasses override this method to yield child expressions.
:return: Iterator over sub-expressions
:rtype: Iterator[Expr]
"""
yield from []
def _iter_all_subs(self) -> Iterator['Expr']:
"""
Recursively iterate over all sub-expressions including this expression.
:return: Iterator over all sub-expressions
:rtype: Iterator[Expr]
"""
yield self
for sub in self._iter_subs():
yield from sub._iter_all_subs()
[docs]
def list_variables(self) -> List['Variable']:
"""
List all unique variables used in this expression.
Variables are identified by name, and the first occurrence of each name
is preserved in the returned list.
:return: List of unique :class:`Variable` objects
:rtype: list[Variable]
"""
vs, retval = set(), []
for item in self._iter_all_subs():
if isinstance(item, Variable) and item.name not in vs:
retval.append(item)
vs.add(item.name)
return retval
def _call(self, **kwargs: Any) -> Any:
"""
Internal method to evaluate the expression with given variable values.
:param kwargs: Variable name to value mapping
:return: Result of the expression evaluation
:raises NotImplementedError: Must be implemented by subclasses
"""
raise NotImplementedError # pragma: no cover
[docs]
def __call__(self, **kwargs: Any) -> Any:
"""
Evaluate the expression with given variable values.
:param kwargs: Variable name to value mapping
:return: Result of the expression evaluation
"""
return self._call(**kwargs)
[docs]
def __str__(self) -> str:
"""
Get string representation of the expression.
The string representation is derived from the AST node serialization.
:return: String representation
:rtype: str
"""
return str(self.to_ast_node())
[docs]
def to_ast_node(self) -> dsl_nodes.Expr:
"""
Convert this expression to an AST node.
:return: AST node representation
:rtype: dsl_nodes.Expr
:raises NotImplementedError: Must be implemented by subclasses
"""
raise NotImplementedError # pragma: no cover
# Arithmetic operators
[docs]
def __add__(self, other: Any) -> 'BinaryOp':
"""
Addition operator (+).
Creates a binary addition expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x + 5
>>> expr(x=10)
15
"""
return BinaryOp(x=self, op='+', y=parse_expr(other))
[docs]
def __radd__(self, other: Any) -> 'BinaryOp':
"""
Reverse addition operator (+).
Enables addition when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 5 + x
>>> expr(x=10)
15
"""
return BinaryOp(x=parse_expr(other), op='+', y=self)
[docs]
def __sub__(self, other: Any) -> 'BinaryOp':
"""
Subtraction operator (-).
Creates a binary subtraction expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x - 3
>>> expr(x=10)
7
"""
return BinaryOp(x=self, op='-', y=parse_expr(other))
[docs]
def __rsub__(self, other: Any) -> 'BinaryOp':
"""
Reverse subtraction operator (-).
Enables subtraction when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 10 - x
>>> expr(x=3)
7
"""
return BinaryOp(x=parse_expr(other), op='-', y=self)
[docs]
def __mul__(self, other: Any) -> 'BinaryOp':
"""
Multiplication operator (*).
Creates a binary multiplication expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x * 2
>>> expr(x=5)
10
"""
return BinaryOp(x=self, op='*', y=parse_expr(other))
[docs]
def __rmul__(self, other: Any) -> 'BinaryOp':
"""
Reverse multiplication operator (*).
Enables multiplication when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 3 * x
>>> expr(x=4)
12
"""
return BinaryOp(x=parse_expr(other), op='*', y=self)
[docs]
def __truediv__(self, other: Any) -> 'BinaryOp':
"""
Division operator (/).
Creates a binary division expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x / 2
>>> expr(x=10)
5.0
"""
return BinaryOp(x=self, op='/', y=parse_expr(other))
[docs]
def __rtruediv__(self, other: Any) -> 'BinaryOp':
"""
Reverse division operator (/).
Enables division when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 20 / x
>>> expr(x=4)
5.0
"""
return BinaryOp(x=parse_expr(other), op='/', y=self)
[docs]
def __mod__(self, other: Any) -> 'BinaryOp':
"""
Modulo operator (%).
Creates a binary modulo expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x % 3
>>> expr(x=10)
1
"""
return BinaryOp(x=self, op='%', y=parse_expr(other))
[docs]
def __rmod__(self, other: Any) -> 'BinaryOp':
"""
Reverse modulo operator (%).
Enables modulo when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 10 % x
>>> expr(x=3)
1
"""
return BinaryOp(x=parse_expr(other), op='%', y=self)
[docs]
def __pow__(self, other: Any) -> 'BinaryOp':
"""
Power operator (**).
Creates a binary exponentiation expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x ** 2
>>> expr(x=3)
9
"""
return BinaryOp(x=self, op='**', y=parse_expr(other))
[docs]
def __rpow__(self, other: Any) -> 'BinaryOp':
"""
Reverse power operator (**).
Enables exponentiation when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 2 ** x
>>> expr(x=3)
8
"""
return BinaryOp(x=parse_expr(other), op='**', y=self)
# Unary operators
[docs]
def __neg__(self) -> 'UnaryOp':
"""
Unary negation operator (-).
Creates a unary negation expression.
:return: Unary operation expression
:rtype: UnaryOp
Example::
>>> x = Variable("x")
>>> expr = -x
>>> expr(x=5)
-5
"""
return UnaryOp(op='-', x=self)
[docs]
def __pos__(self) -> 'UnaryOp':
"""
Unary positive operator (+).
Creates a unary positive expression.
:return: Unary operation expression
:rtype: UnaryOp
Example::
>>> x = Variable("x")
>>> expr = +x
>>> expr(x=5)
5
"""
return UnaryOp(op='+', x=self)
# Bitwise operators
[docs]
def __lshift__(self, other: Any) -> 'BinaryOp':
"""
Left shift operator (<<).
Creates a binary left shift expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x << 2
>>> expr(x=5)
20
"""
return BinaryOp(x=self, op='<<', y=parse_expr(other))
[docs]
def __rlshift__(self, other: Any) -> 'BinaryOp':
"""
Reverse left shift operator (<<).
Enables left shift when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 5 << x
>>> expr(x=2)
20
"""
return BinaryOp(x=parse_expr(other), op='<<', y=self)
[docs]
def __rshift__(self, other: Any) -> 'BinaryOp':
"""
Right shift operator (>>).
Creates a binary right shift expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x >> 1
>>> expr(x=10)
5
"""
return BinaryOp(x=self, op='>>', y=parse_expr(other))
[docs]
def __rrshift__(self, other: Any) -> 'BinaryOp':
"""
Reverse right shift operator (>>).
Enables right shift when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 20 >> x
>>> expr(x=2)
5
"""
return BinaryOp(x=parse_expr(other), op='>>', y=self)
[docs]
def __and__(self, other: Any) -> 'BinaryOp':
"""
Bitwise AND operator (&).
Creates a binary bitwise AND expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x & 0xFF
>>> expr(x=0x1234)
52
"""
return BinaryOp(x=self, op='&', y=parse_expr(other))
[docs]
def __rand__(self, other: Any) -> 'BinaryOp':
"""
Reverse bitwise AND operator (&).
Enables bitwise AND when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 0xFF & x
>>> expr(x=0x1234)
52
"""
return BinaryOp(x=parse_expr(other), op='&', y=self)
[docs]
def __or__(self, other: Any) -> 'BinaryOp':
"""
Bitwise OR operator (|).
Creates a binary bitwise OR expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x | 0x0F
>>> expr(x=0xF0)
255
"""
return BinaryOp(x=self, op='|', y=parse_expr(other))
[docs]
def __ror__(self, other: Any) -> 'BinaryOp':
"""
Reverse bitwise OR operator (|).
Enables bitwise OR when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 0xF0 | x
>>> expr(x=0x0F)
255
"""
return BinaryOp(x=parse_expr(other), op='|', y=self)
[docs]
def __xor__(self, other: Any) -> 'BinaryOp':
"""
Bitwise XOR operator (^).
Creates a binary bitwise XOR expression. The operand is automatically
converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x ^ 0xFF
>>> expr(x=0xAA)
85
"""
return BinaryOp(x=self, op='^', y=parse_expr(other))
[docs]
def __rxor__(self, other: Any) -> 'BinaryOp':
"""
Reverse bitwise XOR operator (^).
Enables bitwise XOR when the expression is on the right side.
The operand is automatically converted using :func:`parse_expr`.
:param other: Left operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = 0xFF ^ x
>>> expr(x=0xAA)
85
"""
return BinaryOp(x=parse_expr(other), op='^', y=self)
# Comparison operators
[docs]
def __lt__(self, other: Any) -> 'BinaryOp':
"""
Less than operator (<).
Creates a binary less than comparison expression. The operand is
automatically converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x < 10
>>> expr(x=5)
True
"""
return BinaryOp(x=self, op='<', y=parse_expr(other))
[docs]
def __le__(self, other: Any) -> 'BinaryOp':
"""
Less than or equal operator (<=).
Creates a binary less than or equal comparison expression. The operand
is automatically converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x <= 10
>>> expr(x=10)
True
"""
return BinaryOp(x=self, op='<=', y=parse_expr(other))
[docs]
def __gt__(self, other: Any) -> 'BinaryOp':
"""
Greater than operator (>).
Creates a binary greater than comparison expression. The operand is
automatically converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x > 5
>>> expr(x=10)
True
"""
return BinaryOp(x=self, op='>', y=parse_expr(other))
[docs]
def __ge__(self, other: Any) -> 'BinaryOp':
"""
Greater than or equal operator (>=).
Creates a binary greater than or equal comparison expression. The operand
is automatically converted using :func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x >= 5
>>> expr(x=5)
True
"""
return BinaryOp(x=self, op='>=', y=parse_expr(other))
# Note: __eq__ and __ne__ are not overridden as they conflict with Python's
# object equality comparison and dataclass equality. Use eq() and ne() methods instead.
[docs]
def eq(self, other: Any) -> 'BinaryOp':
"""
Equality comparison (==).
This method creates an equality comparison expression. It cannot use
the ``__eq__`` magic method as that conflicts with Python's object
equality comparison. The operand is automatically converted using
:func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x.eq(5) # Creates: x == 5
>>> expr(x=5)
True
>>> expr(x=10)
False
"""
return BinaryOp(x=self, op='==', y=parse_expr(other))
[docs]
def ne(self, other: Any) -> 'BinaryOp':
"""
Not equal comparison (!=).
This method creates a not-equal comparison expression. It cannot use
the ``__ne__`` magic method as that conflicts with Python's object
inequality comparison. The operand is automatically converted using
:func:`parse_expr`.
:param other: Right operand (Expr, literal, string, or AST node)
:type other: Any
:return: Binary operation expression
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> expr = x.ne(5) # Creates: x != 5
>>> expr(x=10)
True
>>> expr(x=5)
False
"""
return BinaryOp(x=self, op='!=', y=parse_expr(other))
# Logical operators
# Note: Python's 'and'/'or' keywords cannot be overloaded, and __and__/__or__
# are already used for bitwise operators. Use and_/or_/not_ methods instead.
[docs]
def and_(self, other: Any) -> 'BinaryOp':
"""
Logical AND (&&).
This method creates a logical AND expression. It cannot use the ``&``
operator as that is reserved for bitwise AND operations.
:param other: Right operand
:type other: Any
:return: Binary operation expression with '&&' operator
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> y = Variable("y")
>>> expr = x.gt(0).and_(y.gt(0)) # Creates: (x > 0) && (y > 0)
>>> expr(x=5, y=10)
True
>>> expr(x=5, y=-1)
False
"""
return BinaryOp(x=self, op='&&', y=parse_expr(other))
[docs]
def or_(self, other: Any) -> 'BinaryOp':
"""
Logical OR (||).
This method creates a logical OR expression. It cannot use the ``|``
operator as that is reserved for bitwise OR operations.
:param other: Right operand
:type other: Any
:return: Binary operation expression with '||' operator
:rtype: BinaryOp
Example::
>>> x = Variable("x")
>>> y = Variable("y")
>>> expr = x.gt(0).or_(y.gt(0)) # Creates: (x > 0) || (y > 0)
>>> expr(x=5, y=-1)
True
>>> expr(x=-1, y=-2)
False
"""
return BinaryOp(x=self, op='||', y=parse_expr(other))
[docs]
def not_(self) -> 'UnaryOp':
"""
Logical NOT (!).
This method creates a logical NOT expression. Python's ``not`` keyword
cannot be overloaded, so this method provides the functionality.
:return: Unary operation expression with '!' operator
:rtype: UnaryOp
Example::
>>> x = Variable("x")
>>> expr = x.eq(0).not_() # Creates: !(x == 0)
>>> expr(x=5)
True
>>> expr(x=0)
False
"""
return UnaryOp(op='!', x=self)
# Conditional/ternary operator
[docs]
def select(self, true_value: Any, false_value: Any) -> 'ConditionalOp':
"""
Conditional (ternary) operator (? :).
This method creates a conditional expression that evaluates to
``true_value`` if the condition is true, otherwise ``false_value``.
:param true_value: Expression to evaluate if condition is true
:type true_value: Any
:param false_value: Expression to evaluate if condition is false
:type false_value: Any
:return: Conditional operation expression
:rtype: ConditionalOp
Example::
>>> x = Variable("x")
>>> expr = x.gt(0).select(x, -x) # Creates: (x > 0) ? x : -x
>>> expr(x=5)
5
>>> expr(x=-3)
3
"""
return ConditionalOp(
cond=self,
if_true=parse_expr(true_value),
if_false=parse_expr(false_value)
)
[docs]
def if_then_else(self, true_value: Any, false_value: Any) -> 'ConditionalOp':
"""
Conditional (ternary) operator (? :) - alias for select.
This is an alias for :meth:`select`. It creates a conditional
expression that evaluates to ``true_value`` if the condition is
true, otherwise ``false_value``.
:param true_value: Expression to evaluate if condition is true
:type true_value: Any
:param false_value: Expression to evaluate if condition is false
:type false_value: Any
:return: Conditional operation expression
:rtype: ConditionalOp
Example::
>>> x = Variable("x")
>>> expr = x.gt(0).if_then_else(x, -x) # Creates: (x > 0) ? x : -x
>>> expr(x=5)
5
>>> expr(x=-3)
3
"""
return self.select(true_value, false_value)
[docs]
@dataclass
class Integer(Expr):
"""
Integer literal expression.
:param value: Integer value
:type value: int
"""
value: int
def _call(self, **kwargs: Any) -> int:
"""
Return the integer value.
:param kwargs: Ignored
:return: Integer value
:rtype: int
"""
return self.value
[docs]
def to_ast_node(self) -> dsl_nodes.Expr:
"""
Convert to an Integer AST node.
:return: Integer AST node
:rtype: dsl_nodes.Integer
"""
return dsl_nodes.Integer(raw=str(int(self.value)))
[docs]
@dataclass
class Float(Expr):
"""
Floating point literal expression.
:param value: Float value
:type value: float
"""
value: float
def _call(self, **kwargs: Any) -> float:
"""
Return the float value.
:param kwargs: Ignored
:return: Float value
:rtype: float
"""
return self.value
[docs]
def to_ast_node(self) -> dsl_nodes.Expr:
"""
Convert to a Float AST node or a Constant node for special values.
Recognizes mathematical constants like ``pi``, ``E``, and ``tau`` by
comparing the stored value with these constants.
:return: Float or Constant AST node
:rtype: dsl_nodes.Expr
"""
const_name = None
if abs(self.value - math.pi) < 1e-10:
const_name = 'pi'
elif abs(self.value - math.e) < 1e-10:
const_name = 'E'
elif abs(self.value - math.tau) < 1e-10:
const_name = 'tau'
if const_name is None:
return dsl_nodes.Float(raw=str(float(self.value)))
else:
return dsl_nodes.Constant(raw=const_name)
[docs]
@dataclass
class Boolean(Expr):
"""
Boolean literal expression.
:param value: Boolean value
:type value: bool
"""
value: bool
[docs]
def __post_init__(self) -> None:
"""
Ensure the value is a boolean.
"""
self.value = bool(self.value)
def _call(self, **kwargs: Any) -> bool:
"""
Return the boolean value.
:param kwargs: Ignored
:return: Boolean value
:rtype: bool
"""
return self.value
[docs]
def to_ast_node(self) -> dsl_nodes.Expr:
"""
Convert to a Boolean AST node.
:return: Boolean AST node
:rtype: dsl_nodes.Boolean
"""
return dsl_nodes.Boolean(raw=str(self.value).lower())
_OP_PRECEDENCE = {
# Parentheses (highest precedence)
"()": 100,
# Function calls
"function_call": 90,
# Unary operators
"unary+": 80,
"unary-": 80,
"!": 80,
"not": 80,
# Exponentiation (right associative)
"**": 70,
# Multiplicative operators
"*": 60,
"/": 60,
"%": 60,
# Additive operators
"+": 50,
"-": 50,
# Bitwise shift operators
"<<": 40,
">>": 40,
# Bitwise AND
"&": 35,
# Bitwise XOR
"^": 30,
# Bitwise OR
"|": 25,
# Comparison operators
"<": 20,
">": 20,
"<=": 20,
">=": 20,
"==": 20,
"!=": 20,
# Logical operators
"&&": 15,
"and": 15,
"||": 10,
"or": 10,
# Conditional/ternary operator (C-style)
"?:": 5
}
_OP_FUNCTIONS = {
# Unary operators
"unary+": operator.pos,
"unary-": operator.neg,
"!": lambda x: not bool(x),
"not": lambda x: not bool(x),
# Binary operators
"**": operator.pow,
"*": operator.mul,
"/": operator.truediv,
"%": operator.mod,
"+": operator.add,
"-": operator.sub,
"<<": operator.lshift,
">>": operator.rshift,
"&": operator.and_,
"^": operator.xor,
"|": operator.or_,
"<": operator.lt,
">": operator.gt,
"<=": operator.le,
">=": operator.ge,
"==": operator.eq,
"!=": operator.ne,
"&&": lambda x, y: bool(x) and bool(y),
"and": lambda x, y: bool(x) and bool(y),
"||": lambda x, y: bool(x) or bool(y),
"or": lambda x, y: bool(x) or bool(y),
# Ternary operator
"?:": lambda condition, true_value, false_value: true_value if condition else false_value
}
[docs]
@dataclass
class Op(Expr):
"""
Base class for all operator expressions.
This abstract class provides common functionality for operator expressions.
"""
@property
def op_mark(self) -> str:
"""
Get the operator mark for precedence lookup.
:return: Operator mark
:rtype: str
:raises NotImplementedError: Must be implemented by subclasses
"""
raise NotImplementedError # pragma: no cover
[docs]
@dataclass
class BinaryOp(Op):
"""
Binary operator expression.
:param x: Left operand
:type x: Expr
:param op: Operator symbol
:type op: str
:param y: Right operand
:type y: Expr
"""
__aliases__ = {
'and': '&&',
'or': '||',
}
x: Expr
op: str
y: Expr
[docs]
def __post_init__(self) -> None:
"""
Normalize operator aliases.
"""
self.op = self.__aliases__.get(self.op, self.op)
@property
def op_mark(self) -> str:
"""
Get the operator mark for precedence lookup.
:return: Operator mark
:rtype: str
"""
return self.op
def _iter_subs(self) -> Iterator[Expr]:
"""
Iterate over operands.
:return: Iterator over operands
:rtype: Iterator[Expr]
"""
yield self.x
yield self.y
def _call(self, **kwargs: Any) -> Any:
"""
Evaluate the binary operation.
:param kwargs: Variable name to value mapping
:return: Result of the operation
"""
return _OP_FUNCTIONS[self.op_mark](self.x._call(**kwargs), self.y._call(**kwargs))
[docs]
def to_ast_node(self) -> dsl_nodes.Expr:
"""
Convert to a BinaryOp AST node.
Handles operator precedence by adding parentheses where needed.
:return: BinaryOp AST node
:rtype: dsl_nodes.BinaryOp
"""
my_pre = _OP_PRECEDENCE[self.op_mark]
left_need_paren = False
if isinstance(self.x, Op):
left_pre = _OP_PRECEDENCE[self.x.op_mark]
if left_pre < my_pre:
left_need_paren = True
right_need_paren = False
if isinstance(self.y, Op):
right_pre = _OP_PRECEDENCE[self.y.op_mark]
if right_pre <= my_pre:
right_need_paren = True
left_term = self.x.to_ast_node()
if left_need_paren:
left_term = dsl_nodes.Paren(left_term)
right_term = self.y.to_ast_node()
if right_need_paren:
right_term = dsl_nodes.Paren(right_term)
return dsl_nodes.BinaryOp(
expr1=left_term,
op=self.op,
expr2=right_term,
)
[docs]
@dataclass
class UnaryOp(Op):
"""
Unary operator expression.
:param op: Operator symbol
:type op: str
:param x: Operand
:type x: Expr
"""
__aliases__ = {
'not': '!',
}
op: str
x: Expr
[docs]
def __post_init__(self) -> None:
"""
Normalize operator aliases.
"""
self.op = self.__aliases__.get(self.op, self.op)
@property
def op_mark(self) -> str:
"""
Get the operator mark for precedence lookup.
:return: Operator mark
:rtype: str
"""
return f'unary{self.op}' if self.op in {'+', '-'} else self.op
def _iter_subs(self) -> Iterator[Expr]:
"""
Iterate over operands.
:return: Iterator over operands
:rtype: Iterator[Expr]
"""
yield self.x
def _call(self, **kwargs: Any) -> Any:
"""
Evaluate the unary operation.
:param kwargs: Variable name to value mapping
:return: Result of the operation
"""
return _OP_FUNCTIONS[self.op_mark](self.x._call(**kwargs))
[docs]
def to_ast_node(self) -> dsl_nodes.Expr:
"""
Convert to a UnaryOp AST node.
Handles operator precedence by adding parentheses where needed.
:return: UnaryOp AST node
:rtype: dsl_nodes.UnaryOp
"""
my_pre = _OP_PRECEDENCE[self.op_mark]
x_node = self.x.to_ast_node()
if isinstance(self.x, Op):
value_pre = _OP_PRECEDENCE[self.x.op_mark]
if value_pre <= my_pre:
x_node = dsl_nodes.Paren(expr=x_node)
return dsl_nodes.UnaryOp(op=self.op, expr=x_node)
_MATH_FUNCTIONS = {
# Trigonometric functions
"sin": math.sin,
"cos": math.cos,
"tan": math.tan,
"asin": math.asin,
"acos": math.acos,
"atan": math.atan,
# Hyperbolic functions
"sinh": math.sinh,
"cosh": math.cosh,
"tanh": math.tanh,
"asinh": math.asinh,
"acosh": math.acosh,
"atanh": math.atanh,
# Root and power functions
"sqrt": math.sqrt,
"cbrt": lambda x: math.pow(x, 1 / 3), # Cube root implementation
"exp": math.exp,
# Logarithmic functions
"log": math.log, # Natural logarithm (base e)
"log10": math.log10,
"log2": math.log2,
"log1p": math.log1p, # log(1+x)
# Rounding and absolute value functions
"abs": abs, # Python's built-in abs function
"ceil": math.ceil,
"floor": math.floor,
"round": round, # Python's built-in round function
"trunc": math.trunc,
# Sign function
"sign": lambda x: 0 if x == 0 else (1 if x > 0 else -1) # Returns the sign of x
}
[docs]
@dataclass
class UFunc(Expr):
"""
Mathematical function expression.
Represents calls to mathematical functions like ``sin``, ``cos``, and ``sqrt``.
:param func: Function name
:type func: str
:param x: Function argument
:type x: Expr
"""
func: str
x: Expr
def _iter_subs(self) -> Iterator[Expr]:
"""
Iterate over function arguments.
:return: Iterator over arguments
:rtype: Iterator[Expr]
"""
yield self.x
def _call(self, **kwargs: Any) -> Any:
"""
Evaluate the function.
:param kwargs: Variable name to value mapping
:return: Result of the function call
"""
return _MATH_FUNCTIONS[self.func](self.x._call(**kwargs))
[docs]
def to_ast_node(self) -> dsl_nodes.Expr:
"""
Convert to a UFunc AST node.
:return: UFunc AST node
:rtype: dsl_nodes.UFunc
"""
return dsl_nodes.UFunc(func=self.func, expr=self.x.to_ast_node())
[docs]
@dataclass
class ConditionalOp(Op):
"""
Conditional (ternary) operator expression.
:param cond: Condition expression
:type cond: Expr
:param if_true: Expression to evaluate if condition is true
:type if_true: Expr
:param if_false: Expression to evaluate if condition is false
:type if_false: Expr
"""
cond: Expr
if_true: Expr
if_false: Expr
@property
def op_mark(self) -> str:
"""
Get the operator mark for precedence lookup.
:return: Operator mark
:rtype: str
"""
return '?:'
def _iter_subs(self) -> Iterator[Expr]:
"""
Iterate over sub-expressions.
:return: Iterator over sub-expressions
:rtype: Iterator[Expr]
"""
yield self.cond
yield self.if_true
yield self.if_false
def _call(self, **kwargs: Any) -> Any:
"""
Evaluate the conditional operation.
:param kwargs: Variable name to value mapping
:return: Result of either if_true or if_false based on condition
"""
cond_value = self.cond._call(**kwargs)
if cond_value:
return self.if_true._call(**kwargs)
else:
return self.if_false._call(**kwargs)
[docs]
def to_ast_node(self) -> dsl_nodes.Expr:
"""
Convert to a ConditionalOp AST node.
Handles operator precedence by adding parentheses where needed.
:return: ConditionalOp AST node
:rtype: dsl_nodes.ConditionalOp
"""
my_pre = _OP_PRECEDENCE[self.op_mark]
true_need_paren = False
if isinstance(self.if_true, Op):
true_pre = _OP_PRECEDENCE[self.if_true.op_mark]
if true_pre <= my_pre:
true_need_paren = True
false_need_paren = False
if isinstance(self.if_false, Op):
false_pre = _OP_PRECEDENCE[self.if_false.op_mark]
if false_pre <= my_pre:
false_need_paren = True
cond_term = self.cond.to_ast_node()
true_term = self.if_true.to_ast_node()
if true_need_paren:
true_term = dsl_nodes.Paren(true_term)
false_term = self.if_false.to_ast_node()
if false_need_paren:
false_term = dsl_nodes.Paren(false_term)
return dsl_nodes.ConditionalOp(
cond=cond_term,
value_true=true_term,
value_false=false_term,
)
[docs]
@dataclass
class Variable(Expr):
"""
Variable reference expression.
:param name: Variable name
:type name: str
"""
name: str
def _call(self, **kwargs: Any) -> Any:
"""
Lookup the variable value from kwargs.
:param kwargs: Variable name to value mapping
:return: Variable value
:raises KeyError: If variable name is not found in kwargs
"""
return kwargs[self.name]
[docs]
def to_ast_node(self) -> dsl_nodes.Expr:
"""
Convert to a Name AST node.
:return: Name AST node
:rtype: dsl_nodes.Name
"""
return dsl_nodes.Name(name=self.name)
[docs]
def parse_expr_node_to_expr(node: dsl_nodes.Expr) -> Expr:
"""
Parse an AST expression node into an :class:`Expr` object.
This function converts DSL expression nodes into the corresponding expression
objects. Literal nodes become literal expressions, operators are mapped to
their corresponding expression classes, and parentheses are flattened.
:param node: AST expression node
:type node: dsl_nodes.Expr
:return: Corresponding expression object
:rtype: Expr
:raises TypeError: If the node type is not recognized
Example::
>>> ast_node = dsl_nodes.Integer(raw="42")
>>> expr = parse_expr_node_to_expr(ast_node)
>>> isinstance(expr, Integer)
True
>>> expr.value
42
"""
if isinstance(node, dsl_nodes.Name):
return Variable(name=node.name)
elif isinstance(node, (dsl_nodes.Integer, dsl_nodes.HexInt)):
return Integer(value=node.value)
elif isinstance(node, (dsl_nodes.Constant, dsl_nodes.Float)):
return Float(value=node.value)
elif isinstance(node, dsl_nodes.Boolean):
return Boolean(value=node.value)
elif isinstance(node, dsl_nodes.Paren):
return parse_expr_node_to_expr(node.expr)
elif isinstance(node, dsl_nodes.UnaryOp):
return UnaryOp(
op=node.op,
x=parse_expr_node_to_expr(node.expr),
)
elif isinstance(node, dsl_nodes.BinaryOp):
return BinaryOp(
x=parse_expr_node_to_expr(node.expr1),
op=node.op,
y=parse_expr_node_to_expr(node.expr2),
)
elif isinstance(node, dsl_nodes.ConditionalOp):
return ConditionalOp(
cond=parse_expr_node_to_expr(node.cond),
if_true=parse_expr_node_to_expr(node.value_true),
if_false=parse_expr_node_to_expr(node.value_false),
)
elif isinstance(node, dsl_nodes.UFunc):
return UFunc(
func=node.func,
x=parse_expr_node_to_expr(node.expr),
)
else:
raise TypeError(f'Unknown node type - {node!r}.') # pragma: no cover
[docs]
def parse_expr_from_string(expr_string: str, mode: Literal['generic', 'numeric', 'logical'] = 'generic') -> Expr:
"""
Parse a DSL expression string into an :class:`Expr` object.
This function parses a DSL expression string using one of three grammar entry points
based on the specified mode:
* ``'generic'`` - Uses ``generic_expression`` rule (accepts both numeric and conditional expressions)
* ``'numeric'`` - Uses ``num_expression`` rule (arithmetic, bitwise, variables, functions, ternary)
* ``'logical'`` - Uses ``cond_expression`` rule (comparisons, logical operators, boolean literals)
:param expr_string: DSL expression string to parse
:type expr_string: str
:param mode: Parsing mode, one of ``'generic'``, ``'numeric'``, or ``'logical'``, defaults to ``'generic'``
:type mode: str, optional
:return: Parsed expression object
:rtype: Expr
:raises ValueError: If mode is not one of the valid options
:raises pyfcstm.dsl.error.GrammarParseError: If parsing fails
Example::
>>> from pyfcstm.model.expr import parse_expr_from_string
>>> # Generic mode (default) - accepts both numeric and logical
>>> expr = parse_expr_from_string("x + 5")
>>> isinstance(expr, BinaryOp)
True
>>> expr = parse_expr_from_string("x > 5 && y < 10")
>>> isinstance(expr, BinaryOp)
True
>>> # Numeric mode - arithmetic and bitwise operations
>>> expr = parse_expr_from_string("x * 2 + 3", mode='numeric')
>>> isinstance(expr, BinaryOp)
True
>>> expr = parse_expr_from_string("sqrt(x ** 2 + y ** 2)", mode='numeric')
>>> isinstance(expr, UFunc)
True
>>> # Logical mode - boolean expressions
>>> expr = parse_expr_from_string("x > 5 && y < 10", mode='logical')
>>> isinstance(expr, BinaryOp)
True
>>> expr = parse_expr_from_string("!flag || (a == b)", mode='logical')
>>> isinstance(expr, BinaryOp)
True
"""
from ..dsl.parse import parse_with_grammar_entry
# Map mode to grammar entry point
mode_to_entry = {
'generic': 'generic_expression',
'numeric': 'num_expression',
'logical': 'cond_expression',
}
if mode not in mode_to_entry:
raise ValueError(
f"Invalid mode '{mode}'. Must be one of: {', '.join(repr(m) for m in mode_to_entry.keys())}"
)
entry_point = mode_to_entry[mode]
ast_node = parse_with_grammar_entry(expr_string, entry_point)
return parse_expr_node_to_expr(ast_node)
[docs]
def parse_expr(
expr_input: Any,
mode: Literal['generic', 'numeric', 'logical'] = 'generic'
) -> Expr:
"""
Parse various input types into an :class:`Expr` object.
This function provides a unified interface for parsing expressions from multiple
input types. It accepts DSL AST nodes, expression strings, or existing Expr objects.
:param expr_input: Input to parse - can be:
- :class:`Expr` object: returned directly without modification
- :class:`dsl_nodes.Expr` AST node: converted using :func:`parse_expr_node_to_expr`
- :class:`str`: parsed using :func:`parse_expr_from_string` with the specified mode
- :class:`bool`: converted to :class:`Boolean` literal
- :class:`int`: converted to :class:`Integer` literal
- :class:`float`: converted to :class:`Float` literal
:type expr_input: Any
:param mode: Parsing mode for string inputs, one of ``'generic'``, ``'numeric'``, or ``'logical'``, defaults to ``'generic'``
:type mode: Literal['generic', 'numeric', 'logical'], optional
:return: Parsed expression object
:rtype: Expr
:raises TypeError: If input type is not supported
:raises ValueError: If mode is invalid (for string inputs)
:raises pyfcstm.dsl.error.GrammarParseError: If string parsing fails
.. warning::
The ``mode`` parameter only affects string inputs. If a non-default mode is
specified with an Expr object or AST node input, a warning will be issued.
Example::
>>> from pyfcstm.model.expr import parse_expr, Variable, Integer, BinaryOp
>>> from pyfcstm.dsl.node import Integer as IntNode
>>> # Parse from Expr object (returns directly)
>>> expr_obj = BinaryOp(x=Variable("x"), op="+", y=Integer(5))
>>> result = parse_expr(expr_obj)
>>> result is expr_obj
True
>>> # Parse from DSL AST node
>>> ast_node = IntNode(raw="42")
>>> expr = parse_expr(ast_node)
>>> isinstance(expr, Integer)
True
>>> # Parse from string
>>> expr = parse_expr("x + 5")
>>> isinstance(expr, BinaryOp)
True
>>> expr = parse_expr("x > 5 && y < 10", mode='logical')
>>> isinstance(expr, BinaryOp)
True
>>> # Parse from Python literals
>>> expr = parse_expr(42)
>>> isinstance(expr, Integer)
True
>>> expr = parse_expr(3.14)
>>> isinstance(expr, Float)
True
>>> expr = parse_expr(True)
>>> isinstance(expr, Boolean)
True
"""
# Check if mode is non-default for non-string inputs
if mode != 'generic' and not isinstance(expr_input, str):
warnings.warn(
f"The 'mode' parameter ({mode!r}) has no effect for non-string inputs. "
f"It only applies when parsing from strings.",
UserWarning,
stacklevel=2
)
# If already an Expr object, return directly
if isinstance(expr_input, Expr):
return expr_input
# If DSL AST node, convert to Expr
if isinstance(expr_input, dsl_nodes.Expr):
return parse_expr_node_to_expr(expr_input)
# If string, parse with specified mode
if isinstance(expr_input, str):
return parse_expr_from_string(expr_input, mode=mode)
# If bool (must check before int, as bool is subclass of int)
if isinstance(expr_input, bool):
return Boolean(value=expr_input)
# If int
if isinstance(expr_input, int):
return Integer(value=expr_input)
# If float
if isinstance(expr_input, float):
return Float(value=expr_input)
# Unsupported type
raise TypeError(
f"Unsupported input type: {type(expr_input).__name__}. "
f"Expected Expr, dsl_nodes.Expr, str, bool, int, or float."
)