diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 022be60c..d121bf8b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to GemsPy are documented here. ## [Unreleased] ### Added +- **Power operator `^` in the expression language** - right-associative, binding + tighter than unary minus and than `*` `/`, so `-2^2` is `-(2^2)`. Operands + must be literals or parameters, except in `extra-outputs` where a variable may + be raised to a power. The Python API gains `**` on `ExpressionNode`. + - **Taxonomy conformance checked when a study is loaded** - `load_study` reads the optional `input/taxonomy.yml` and calls `validate_libraries_against_taxonomy` on every library declaring a `taxonomy` diff --git a/grammar/Expr.g4 b/grammar/Expr.g4 index f990402f..a889931d 100644 --- a/grammar/Expr.g4 +++ b/grammar/Expr.g4 @@ -20,6 +20,7 @@ fullexpr: expr EOF; expr : atom # unsignedAtom | portFieldExpr # portField + | expr '^' expr # power | '-' expr # negation | '(' expr ')' # expression | expr op=('/' | '*') expr # muldiv @@ -53,6 +54,15 @@ shift: TIME shift_expr?; // A shift expression can only be extended to the right by a // "right_expr" which cannot start with a + or -, // unlike shift_expr itself. +// +// Unlike "expr", which is one left-recursive rule where precedence falls out +// of the order of the alternatives, this sub-grammar is a hand-written cascade: +// precedence is encoded in which rule each operand recurses into. An operator +// therefore has to be given its precedence twice, once in "expr" and once here, +// and the two must agree — "x[t-2^2]" is expected to shift by the value of the +// expression "-2^2". Adding an operator to "expr" alone gives it nothing inside +// a shift. +// // TODO: the grammar is still a little weird, because we // allow more things in the "expr" parts of those // shift expressions than on their left-most part @@ -60,13 +70,40 @@ shift: TIME shift_expr?; shift_expr : shift_expr op=('*' | '/') right_expr # shiftMuldiv | shift_expr op=('+' | '-') right_expr # shiftAddsub - | op=('+' | '-') atom # signedAtom - | op=('+' | '-') '(' expr ')' # signedExpression + | op=('+' | '-') shift_operand # signedOperand ; right_expr : right_expr op=('/' | '*') right_expr # rightMuldiv - | '(' expr ')' # rightExpression + | shift_operand # rightOperand + ; + +// The two highest precedence tiers of the shift sub-grammar, mirroring the +// "power" and atom levels of "expr". They are named after their level, not +// after '^': every operand of a shift goes through them, whether or not a +// power is involved. +// +// They must stay separate from "right_expr" for two reasons, both of them +// restating here what "expr" gets for free from its alternative order: +// - the leading sign of "shift_expr" needs a power-capable operand, so that +// '^' binds tighter than the sign and "t - 2^2" shifts by -4, matching +// "-2^2" = "-(2^2)". That operand must NOT also swallow '*' and '/', or +// "t - 2*3" becomes ambiguous and reassociates to "-(2*3)" instead of +// "(-2)*3"; +// - "right_expr" is the muldiv tier, so on the right of '^' it would be +// entered at precedence 0 and greedily swallow '*' and '/': "t - 2^2*3" +// would mean "2^(2*3)" instead of "(2^2)*3". +// +// The one deliberate divergence from "expr" is that the exponent is unsigned +// here, so "t + 2^-1" is a parse error: a fractional time shift is +// meaningless, while negative exponents remain available in "expr". +shift_operand + : shift_primary '^' shift_operand # rightPower + | shift_primary # rightPrimary + ; + +shift_primary + : '(' expr ')' # rightExpression | atom # rightAtom ; diff --git a/grammar/README.md b/grammar/README.md index 5d51d0b5..e097a15d 100644 --- a/grammar/README.md +++ b/grammar/README.md @@ -4,11 +4,11 @@ use for defining constraints, objective, etc. [ANTLR](https://www.antlr.org) needs to be used to generate the associated -parser code, which must be written to [andromede.expression.parsing.antlr](/src/andromede/expression/parsing/antlr) +parser code, which must be written to [gems_craft.expression.parsing.antlr](/src/gems_craft/expression/parsing/antlr) package. **No other files are expected to be present in that package**. To achieve this you may use the provided `generate-parser.sh` script after having installed -antlr4-tools (`pip install -r requirements-dev.txt` in root directory). +antlr4-tools (`uv sync --group dev` in root directory). You may also, for example, use the ANTLR4 PyCharm plugin. diff --git a/grammar/generate-parser.sh b/grammar/generate-parser.sh index 74aa2907..caaa898c 100755 --- a/grammar/generate-parser.sh +++ b/grammar/generate-parser.sh @@ -2,4 +2,4 @@ script_file=$(readlink -f -- "$0") script_dir=$(dirname -- "${script_file}") -antlr4 -Dlanguage=Python3 -Werror -no-listener -visitor -o ${script_dir}/../src/gems/expression/parsing/antlr Expr.g4 +antlr4 -Dlanguage=Python3 -Werror -no-listener -visitor -o ${script_dir}/../src/gems_craft/expression/parsing/antlr Expr.g4 diff --git a/src/gems_craft/expression/__init__.py b/src/gems_craft/expression/__init__.py index 38e42606..9f00adeb 100644 --- a/src/gems_craft/expression/__init__.py +++ b/src/gems_craft/expression/__init__.py @@ -27,6 +27,7 @@ MultiplicationNode, NegationNode, ParameterNode, + PowerNode, RoundNode, VariableNode, literal, diff --git a/src/gems_craft/expression/degree.py b/src/gems_craft/expression/degree.py index 4fe13ef0..0051b0e9 100644 --- a/src/gems_craft/expression/degree.py +++ b/src/gems_craft/expression/degree.py @@ -41,12 +41,17 @@ MultiplicationNode, NegationNode, ParameterNode, + PowerNode, ScenarioOperatorNode, VariableNode, ) from .visitor import ExpressionVisitor, T, visit +def _is_non_negative_integer(value: float) -> bool: + return value >= 0 and float(value).is_integer() + + class ExpressionDegreeVisitor(ExpressionVisitor[int | float]): """ Computes degree of expression with respect to variables. @@ -72,6 +77,22 @@ def division(self, node: DivisionNode) -> int | float: raise ValueError("Degree computation not implemented for divisions.") return visit(node.left, self) + def power(self, node: PowerNode) -> int | float: + # A variable exponent is never polynomial, and a variable base only + # stays polynomial for a non-negative integer exponent. + if visit(node.right, self) != 0: + return math.inf + base_degree = visit(node.left, self) + if base_degree == 0: + return 0 + if isinstance(node.right, LiteralNode) and _is_non_negative_integer( + node.right.value + ): + exponent = int(node.right.value) + # inf * 0 is nan, whereas anything to the power 0 is the constant 1. + return 0 if exponent == 0 else base_degree * exponent + return math.inf + def comparison(self, node: ComparisonNode) -> int | float: return max(visit(node.left, self), visit(node.right, self)) diff --git a/src/gems_craft/expression/equality.py b/src/gems_craft/expression/equality.py index ede03651..dc819929 100644 --- a/src/gems_craft/expression/equality.py +++ b/src/gems_craft/expression/equality.py @@ -37,6 +37,7 @@ MinNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -77,6 +78,8 @@ def visit(self, left: ExpressionNode, right: ExpressionNode) -> bool: right, MultiplicationNode ): return self.multiplication(left, right) + if isinstance(left, PowerNode) and isinstance(right, PowerNode): + return self.power(left, right) if isinstance(left, ComparisonNode) and isinstance(right, ComparisonNode): return self.comparison(left, right) if isinstance(left, VariableNode) and isinstance(right, VariableNode): @@ -151,6 +154,9 @@ def multiplication( def division(self, left: DivisionNode, right: DivisionNode) -> bool: return self._visit_operands(left, right) + def power(self, left: PowerNode, right: PowerNode) -> bool: + return self._visit_operands(left, right) + def comparison(self, left: ComparisonNode, right: ComparisonNode) -> bool: return left.comparator == right.comparator and self._visit_operands(left, right) diff --git a/src/gems_craft/expression/expression.py b/src/gems_craft/expression/expression.py index 45d65da2..bd0d6f7b 100644 --- a/src/gems_craft/expression/expression.py +++ b/src/gems_craft/expression/expression.py @@ -77,6 +77,12 @@ def __truediv__(self, rhs: Any) -> "ExpressionNode": def __rtruediv__(self, lhs: Any) -> "ExpressionNode": return _apply_if_node(lhs, lambda x: DivisionNode(x, self)) + def __pow__(self, rhs: Any) -> "ExpressionNode": + return _apply_if_node(rhs, lambda x: PowerNode(self, x)) + + def __rpow__(self, lhs: Any) -> "ExpressionNode": + return _apply_if_node(lhs, lambda x: PowerNode(x, self)) + def __le__(self, rhs: Any) -> "ExpressionNode": return _apply_if_node( rhs, lambda x: ComparisonNode(self, x, Comparator.LESS_THAN) @@ -285,6 +291,17 @@ class DivisionNode(BinaryOperatorNode): pass +@dataclass(frozen=True, eq=False) +class PowerNode(BinaryOperatorNode): + """Raises ``left`` to the power ``right``, as written ``left ^ right``. + + ``^`` binds tighter than unary minus, so ``-2^2`` is ``-(2^2)``, and it is + right-associative, so ``2^3^2`` is ``2^(3^2)``. + """ + + pass + + @dataclass(frozen=True, eq=False) class MaxNode(ExpressionNode): operands: List[ExpressionNode] diff --git a/src/gems_craft/expression/indexing.py b/src/gems_craft/expression/indexing.py index 3168b15d..d6fcbf15 100644 --- a/src/gems_craft/expression/indexing.py +++ b/src/gems_craft/expression/indexing.py @@ -35,6 +35,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -96,6 +97,9 @@ def multiplication(self, node: MultiplicationNode) -> IndexingStructure: def division(self, node: DivisionNode) -> IndexingStructure: return self._combine([node.left, node.right]) + def power(self, node: PowerNode) -> IndexingStructure: + return self._combine([node.left, node.right]) + def comparison(self, node: ComparisonNode) -> IndexingStructure: return self._combine([node.left, node.right]) diff --git a/src/gems_craft/expression/parsing/antlr/Expr.interp b/src/gems_craft/expression/parsing/antlr/Expr.interp index d2035f0c..8d302381 100644 --- a/src/gems_craft/expression/parsing/antlr/Expr.interp +++ b/src/gems_craft/expression/parsing/antlr/Expr.interp @@ -1,6 +1,7 @@ token literal names: null '.' +'^' '-' '(' ')' @@ -34,6 +35,7 @@ null null null null +null NUMBER TIME IDENTIFIER @@ -49,7 +51,9 @@ atom shift shift_expr right_expr +shift_operand +shift_primary atn: -[4, 1, 18, 151, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 55, 8, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 82, 8, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 5, 2, 93, 8, 2, 10, 2, 12, 2, 96, 9, 2, 1, 3, 1, 3, 1, 3, 5, 3, 101, 8, 3, 10, 3, 12, 3, 104, 9, 3, 1, 4, 1, 4, 3, 4, 108, 8, 4, 1, 5, 1, 5, 3, 5, 112, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 122, 8, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 5, 6, 130, 8, 6, 10, 6, 12, 6, 133, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 141, 8, 7, 1, 7, 1, 7, 1, 7, 5, 7, 146, 8, 7, 10, 7, 12, 7, 149, 9, 7, 1, 7, 0, 3, 4, 12, 14, 8, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 1, 0, 5, 6, 2, 0, 2, 2, 7, 7, 165, 0, 16, 1, 0, 0, 0, 2, 20, 1, 0, 0, 0, 4, 81, 1, 0, 0, 0, 6, 97, 1, 0, 0, 0, 8, 107, 1, 0, 0, 0, 10, 109, 1, 0, 0, 0, 12, 121, 1, 0, 0, 0, 14, 140, 1, 0, 0, 0, 16, 17, 5, 16, 0, 0, 17, 18, 5, 1, 0, 0, 18, 19, 5, 16, 0, 0, 19, 1, 1, 0, 0, 0, 20, 21, 3, 4, 2, 0, 21, 22, 5, 0, 0, 1, 22, 3, 1, 0, 0, 0, 23, 24, 6, 2, -1, 0, 24, 82, 3, 8, 4, 0, 25, 82, 3, 0, 0, 0, 26, 27, 5, 2, 0, 0, 27, 82, 3, 4, 2, 13, 28, 29, 5, 3, 0, 0, 29, 30, 3, 4, 2, 0, 30, 31, 5, 4, 0, 0, 31, 82, 1, 0, 0, 0, 32, 33, 5, 8, 0, 0, 33, 34, 5, 3, 0, 0, 34, 35, 3, 4, 2, 0, 35, 36, 5, 4, 0, 0, 36, 82, 1, 0, 0, 0, 37, 38, 5, 9, 0, 0, 38, 39, 5, 3, 0, 0, 39, 40, 3, 0, 0, 0, 40, 41, 5, 4, 0, 0, 41, 82, 1, 0, 0, 0, 42, 43, 5, 8, 0, 0, 43, 44, 5, 3, 0, 0, 44, 45, 3, 10, 5, 0, 45, 46, 5, 10, 0, 0, 46, 47, 3, 10, 5, 0, 47, 48, 5, 11, 0, 0, 48, 49, 3, 4, 2, 0, 49, 50, 5, 4, 0, 0, 50, 82, 1, 0, 0, 0, 51, 52, 5, 16, 0, 0, 52, 54, 5, 3, 0, 0, 53, 55, 3, 6, 3, 0, 54, 53, 1, 0, 0, 0, 54, 55, 1, 0, 0, 0, 55, 56, 1, 0, 0, 0, 56, 82, 5, 4, 0, 0, 57, 58, 5, 16, 0, 0, 58, 59, 5, 12, 0, 0, 59, 60, 3, 10, 5, 0, 60, 61, 5, 13, 0, 0, 61, 82, 1, 0, 0, 0, 62, 63, 5, 16, 0, 0, 63, 64, 5, 12, 0, 0, 64, 65, 3, 4, 2, 0, 65, 66, 5, 13, 0, 0, 66, 82, 1, 0, 0, 0, 67, 68, 5, 3, 0, 0, 68, 69, 3, 4, 2, 0, 69, 70, 5, 4, 0, 0, 70, 71, 5, 12, 0, 0, 71, 72, 3, 10, 5, 0, 72, 73, 5, 13, 0, 0, 73, 82, 1, 0, 0, 0, 74, 75, 5, 3, 0, 0, 75, 76, 3, 4, 2, 0, 76, 77, 5, 4, 0, 0, 77, 78, 5, 12, 0, 0, 78, 79, 3, 4, 2, 0, 79, 80, 5, 13, 0, 0, 80, 82, 1, 0, 0, 0, 81, 23, 1, 0, 0, 0, 81, 25, 1, 0, 0, 0, 81, 26, 1, 0, 0, 0, 81, 28, 1, 0, 0, 0, 81, 32, 1, 0, 0, 0, 81, 37, 1, 0, 0, 0, 81, 42, 1, 0, 0, 0, 81, 51, 1, 0, 0, 0, 81, 57, 1, 0, 0, 0, 81, 62, 1, 0, 0, 0, 81, 67, 1, 0, 0, 0, 81, 74, 1, 0, 0, 0, 82, 94, 1, 0, 0, 0, 83, 84, 10, 11, 0, 0, 84, 85, 7, 0, 0, 0, 85, 93, 3, 4, 2, 12, 86, 87, 10, 10, 0, 0, 87, 88, 7, 1, 0, 0, 88, 93, 3, 4, 2, 11, 89, 90, 10, 9, 0, 0, 90, 91, 5, 17, 0, 0, 91, 93, 3, 4, 2, 10, 92, 83, 1, 0, 0, 0, 92, 86, 1, 0, 0, 0, 92, 89, 1, 0, 0, 0, 93, 96, 1, 0, 0, 0, 94, 92, 1, 0, 0, 0, 94, 95, 1, 0, 0, 0, 95, 5, 1, 0, 0, 0, 96, 94, 1, 0, 0, 0, 97, 102, 3, 4, 2, 0, 98, 99, 5, 11, 0, 0, 99, 101, 3, 4, 2, 0, 100, 98, 1, 0, 0, 0, 101, 104, 1, 0, 0, 0, 102, 100, 1, 0, 0, 0, 102, 103, 1, 0, 0, 0, 103, 7, 1, 0, 0, 0, 104, 102, 1, 0, 0, 0, 105, 108, 5, 14, 0, 0, 106, 108, 5, 16, 0, 0, 107, 105, 1, 0, 0, 0, 107, 106, 1, 0, 0, 0, 108, 9, 1, 0, 0, 0, 109, 111, 5, 15, 0, 0, 110, 112, 3, 12, 6, 0, 111, 110, 1, 0, 0, 0, 111, 112, 1, 0, 0, 0, 112, 11, 1, 0, 0, 0, 113, 114, 6, 6, -1, 0, 114, 115, 7, 1, 0, 0, 115, 122, 3, 8, 4, 0, 116, 117, 7, 1, 0, 0, 117, 118, 5, 3, 0, 0, 118, 119, 3, 4, 2, 0, 119, 120, 5, 4, 0, 0, 120, 122, 1, 0, 0, 0, 121, 113, 1, 0, 0, 0, 121, 116, 1, 0, 0, 0, 122, 131, 1, 0, 0, 0, 123, 124, 10, 4, 0, 0, 124, 125, 7, 0, 0, 0, 125, 130, 3, 14, 7, 0, 126, 127, 10, 3, 0, 0, 127, 128, 7, 1, 0, 0, 128, 130, 3, 14, 7, 0, 129, 123, 1, 0, 0, 0, 129, 126, 1, 0, 0, 0, 130, 133, 1, 0, 0, 0, 131, 129, 1, 0, 0, 0, 131, 132, 1, 0, 0, 0, 132, 13, 1, 0, 0, 0, 133, 131, 1, 0, 0, 0, 134, 135, 6, 7, -1, 0, 135, 136, 5, 3, 0, 0, 136, 137, 3, 4, 2, 0, 137, 138, 5, 4, 0, 0, 138, 141, 1, 0, 0, 0, 139, 141, 3, 8, 4, 0, 140, 134, 1, 0, 0, 0, 140, 139, 1, 0, 0, 0, 141, 147, 1, 0, 0, 0, 142, 143, 10, 3, 0, 0, 143, 144, 7, 0, 0, 0, 144, 146, 3, 14, 7, 4, 145, 142, 1, 0, 0, 0, 146, 149, 1, 0, 0, 0, 147, 145, 1, 0, 0, 0, 147, 148, 1, 0, 0, 0, 148, 15, 1, 0, 0, 0, 149, 147, 1, 0, 0, 0, 12, 54, 81, 92, 94, 102, 107, 111, 121, 129, 131, 140, 147] \ No newline at end of file +[4, 1, 19, 161, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 59, 8, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 86, 8, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 5, 2, 100, 8, 2, 10, 2, 12, 2, 103, 9, 2, 1, 3, 1, 3, 1, 3, 5, 3, 108, 8, 3, 10, 3, 12, 3, 111, 9, 3, 1, 4, 1, 4, 3, 4, 115, 8, 4, 1, 5, 1, 5, 3, 5, 119, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 5, 6, 131, 8, 6, 10, 6, 12, 6, 134, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 142, 8, 7, 10, 7, 12, 7, 145, 9, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 152, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 159, 8, 9, 1, 9, 0, 3, 4, 12, 14, 10, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 0, 2, 1, 0, 6, 7, 2, 0, 3, 3, 8, 8, 174, 0, 20, 1, 0, 0, 0, 2, 24, 1, 0, 0, 0, 4, 85, 1, 0, 0, 0, 6, 104, 1, 0, 0, 0, 8, 114, 1, 0, 0, 0, 10, 116, 1, 0, 0, 0, 12, 120, 1, 0, 0, 0, 14, 135, 1, 0, 0, 0, 16, 151, 1, 0, 0, 0, 18, 158, 1, 0, 0, 0, 20, 21, 5, 17, 0, 0, 21, 22, 5, 1, 0, 0, 22, 23, 5, 17, 0, 0, 23, 1, 1, 0, 0, 0, 24, 25, 3, 4, 2, 0, 25, 26, 5, 0, 0, 1, 26, 3, 1, 0, 0, 0, 27, 28, 6, 2, -1, 0, 28, 86, 3, 8, 4, 0, 29, 86, 3, 0, 0, 0, 30, 31, 5, 3, 0, 0, 31, 86, 3, 4, 2, 13, 32, 33, 5, 4, 0, 0, 33, 34, 3, 4, 2, 0, 34, 35, 5, 5, 0, 0, 35, 86, 1, 0, 0, 0, 36, 37, 5, 9, 0, 0, 37, 38, 5, 4, 0, 0, 38, 39, 3, 4, 2, 0, 39, 40, 5, 5, 0, 0, 40, 86, 1, 0, 0, 0, 41, 42, 5, 10, 0, 0, 42, 43, 5, 4, 0, 0, 43, 44, 3, 0, 0, 0, 44, 45, 5, 5, 0, 0, 45, 86, 1, 0, 0, 0, 46, 47, 5, 9, 0, 0, 47, 48, 5, 4, 0, 0, 48, 49, 3, 10, 5, 0, 49, 50, 5, 11, 0, 0, 50, 51, 3, 10, 5, 0, 51, 52, 5, 12, 0, 0, 52, 53, 3, 4, 2, 0, 53, 54, 5, 5, 0, 0, 54, 86, 1, 0, 0, 0, 55, 56, 5, 17, 0, 0, 56, 58, 5, 4, 0, 0, 57, 59, 3, 6, 3, 0, 58, 57, 1, 0, 0, 0, 58, 59, 1, 0, 0, 0, 59, 60, 1, 0, 0, 0, 60, 86, 5, 5, 0, 0, 61, 62, 5, 17, 0, 0, 62, 63, 5, 13, 0, 0, 63, 64, 3, 10, 5, 0, 64, 65, 5, 14, 0, 0, 65, 86, 1, 0, 0, 0, 66, 67, 5, 17, 0, 0, 67, 68, 5, 13, 0, 0, 68, 69, 3, 4, 2, 0, 69, 70, 5, 14, 0, 0, 70, 86, 1, 0, 0, 0, 71, 72, 5, 4, 0, 0, 72, 73, 3, 4, 2, 0, 73, 74, 5, 5, 0, 0, 74, 75, 5, 13, 0, 0, 75, 76, 3, 10, 5, 0, 76, 77, 5, 14, 0, 0, 77, 86, 1, 0, 0, 0, 78, 79, 5, 4, 0, 0, 79, 80, 3, 4, 2, 0, 80, 81, 5, 5, 0, 0, 81, 82, 5, 13, 0, 0, 82, 83, 3, 4, 2, 0, 83, 84, 5, 14, 0, 0, 84, 86, 1, 0, 0, 0, 85, 27, 1, 0, 0, 0, 85, 29, 1, 0, 0, 0, 85, 30, 1, 0, 0, 0, 85, 32, 1, 0, 0, 0, 85, 36, 1, 0, 0, 0, 85, 41, 1, 0, 0, 0, 85, 46, 1, 0, 0, 0, 85, 55, 1, 0, 0, 0, 85, 61, 1, 0, 0, 0, 85, 66, 1, 0, 0, 0, 85, 71, 1, 0, 0, 0, 85, 78, 1, 0, 0, 0, 86, 101, 1, 0, 0, 0, 87, 88, 10, 14, 0, 0, 88, 89, 5, 2, 0, 0, 89, 100, 3, 4, 2, 14, 90, 91, 10, 11, 0, 0, 91, 92, 7, 0, 0, 0, 92, 100, 3, 4, 2, 12, 93, 94, 10, 10, 0, 0, 94, 95, 7, 1, 0, 0, 95, 100, 3, 4, 2, 11, 96, 97, 10, 9, 0, 0, 97, 98, 5, 18, 0, 0, 98, 100, 3, 4, 2, 10, 99, 87, 1, 0, 0, 0, 99, 90, 1, 0, 0, 0, 99, 93, 1, 0, 0, 0, 99, 96, 1, 0, 0, 0, 100, 103, 1, 0, 0, 0, 101, 99, 1, 0, 0, 0, 101, 102, 1, 0, 0, 0, 102, 5, 1, 0, 0, 0, 103, 101, 1, 0, 0, 0, 104, 109, 3, 4, 2, 0, 105, 106, 5, 12, 0, 0, 106, 108, 3, 4, 2, 0, 107, 105, 1, 0, 0, 0, 108, 111, 1, 0, 0, 0, 109, 107, 1, 0, 0, 0, 109, 110, 1, 0, 0, 0, 110, 7, 1, 0, 0, 0, 111, 109, 1, 0, 0, 0, 112, 115, 5, 15, 0, 0, 113, 115, 5, 17, 0, 0, 114, 112, 1, 0, 0, 0, 114, 113, 1, 0, 0, 0, 115, 9, 1, 0, 0, 0, 116, 118, 5, 16, 0, 0, 117, 119, 3, 12, 6, 0, 118, 117, 1, 0, 0, 0, 118, 119, 1, 0, 0, 0, 119, 11, 1, 0, 0, 0, 120, 121, 6, 6, -1, 0, 121, 122, 7, 1, 0, 0, 122, 123, 3, 16, 8, 0, 123, 132, 1, 0, 0, 0, 124, 125, 10, 3, 0, 0, 125, 126, 7, 0, 0, 0, 126, 131, 3, 14, 7, 0, 127, 128, 10, 2, 0, 0, 128, 129, 7, 1, 0, 0, 129, 131, 3, 14, 7, 0, 130, 124, 1, 0, 0, 0, 130, 127, 1, 0, 0, 0, 131, 134, 1, 0, 0, 0, 132, 130, 1, 0, 0, 0, 132, 133, 1, 0, 0, 0, 133, 13, 1, 0, 0, 0, 134, 132, 1, 0, 0, 0, 135, 136, 6, 7, -1, 0, 136, 137, 3, 16, 8, 0, 137, 143, 1, 0, 0, 0, 138, 139, 10, 2, 0, 0, 139, 140, 7, 0, 0, 0, 140, 142, 3, 14, 7, 3, 141, 138, 1, 0, 0, 0, 142, 145, 1, 0, 0, 0, 143, 141, 1, 0, 0, 0, 143, 144, 1, 0, 0, 0, 144, 15, 1, 0, 0, 0, 145, 143, 1, 0, 0, 0, 146, 147, 3, 18, 9, 0, 147, 148, 5, 2, 0, 0, 148, 149, 3, 16, 8, 0, 149, 152, 1, 0, 0, 0, 150, 152, 3, 18, 9, 0, 151, 146, 1, 0, 0, 0, 151, 150, 1, 0, 0, 0, 152, 17, 1, 0, 0, 0, 153, 154, 5, 4, 0, 0, 154, 155, 3, 4, 2, 0, 155, 156, 5, 5, 0, 0, 156, 159, 1, 0, 0, 0, 157, 159, 3, 8, 4, 0, 158, 153, 1, 0, 0, 0, 158, 157, 1, 0, 0, 0, 159, 19, 1, 0, 0, 0, 12, 58, 85, 99, 101, 109, 114, 118, 130, 132, 143, 151, 158] \ No newline at end of file diff --git a/src/gems_craft/expression/parsing/antlr/Expr.tokens b/src/gems_craft/expression/parsing/antlr/Expr.tokens index c8638328..d1c7367f 100644 --- a/src/gems_craft/expression/parsing/antlr/Expr.tokens +++ b/src/gems_craft/expression/parsing/antlr/Expr.tokens @@ -11,22 +11,24 @@ T__9=10 T__10=11 T__11=12 T__12=13 -NUMBER=14 -TIME=15 -IDENTIFIER=16 -COMPARISON=17 -WS=18 +T__13=14 +NUMBER=15 +TIME=16 +IDENTIFIER=17 +COMPARISON=18 +WS=19 '.'=1 -'-'=2 -'('=3 -')'=4 -'/'=5 -'*'=6 -'+'=7 -'sum'=8 -'sum_connections'=9 -'..'=10 -','=11 -'['=12 -']'=13 -'t'=15 +'^'=2 +'-'=3 +'('=4 +')'=5 +'/'=6 +'*'=7 +'+'=8 +'sum'=9 +'sum_connections'=10 +'..'=11 +','=12 +'['=13 +']'=14 +'t'=16 diff --git a/src/gems_craft/expression/parsing/antlr/ExprLexer.interp b/src/gems_craft/expression/parsing/antlr/ExprLexer.interp index d761b1f1..e99a5b5b 100644 --- a/src/gems_craft/expression/parsing/antlr/ExprLexer.interp +++ b/src/gems_craft/expression/parsing/antlr/ExprLexer.interp @@ -1,6 +1,7 @@ token literal names: null '.' +'^' '-' '(' ')' @@ -34,6 +35,7 @@ null null null null +null NUMBER TIME IDENTIFIER @@ -54,6 +56,7 @@ T__9 T__10 T__11 T__12 +T__13 DIGIT CHAR CHAR_OR_DIGIT @@ -71,4 +74,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 18, 127, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 14, 1, 14, 1, 15, 1, 15, 3, 15, 93, 8, 15, 1, 16, 4, 16, 96, 8, 16, 11, 16, 12, 16, 97, 1, 16, 1, 16, 4, 16, 102, 8, 16, 11, 16, 12, 16, 103, 3, 16, 106, 8, 16, 1, 17, 1, 17, 1, 18, 1, 18, 5, 18, 112, 8, 18, 10, 18, 12, 18, 115, 9, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 3, 19, 122, 8, 19, 1, 20, 1, 20, 1, 20, 1, 20, 0, 0, 21, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 0, 29, 0, 31, 0, 33, 14, 35, 15, 37, 16, 39, 17, 41, 18, 1, 0, 3, 1, 0, 48, 57, 3, 0, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 130, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 1, 43, 1, 0, 0, 0, 3, 45, 1, 0, 0, 0, 5, 47, 1, 0, 0, 0, 7, 49, 1, 0, 0, 0, 9, 51, 1, 0, 0, 0, 11, 53, 1, 0, 0, 0, 13, 55, 1, 0, 0, 0, 15, 57, 1, 0, 0, 0, 17, 61, 1, 0, 0, 0, 19, 77, 1, 0, 0, 0, 21, 80, 1, 0, 0, 0, 23, 82, 1, 0, 0, 0, 25, 84, 1, 0, 0, 0, 27, 86, 1, 0, 0, 0, 29, 88, 1, 0, 0, 0, 31, 92, 1, 0, 0, 0, 33, 95, 1, 0, 0, 0, 35, 107, 1, 0, 0, 0, 37, 109, 1, 0, 0, 0, 39, 121, 1, 0, 0, 0, 41, 123, 1, 0, 0, 0, 43, 44, 5, 46, 0, 0, 44, 2, 1, 0, 0, 0, 45, 46, 5, 45, 0, 0, 46, 4, 1, 0, 0, 0, 47, 48, 5, 40, 0, 0, 48, 6, 1, 0, 0, 0, 49, 50, 5, 41, 0, 0, 50, 8, 1, 0, 0, 0, 51, 52, 5, 47, 0, 0, 52, 10, 1, 0, 0, 0, 53, 54, 5, 42, 0, 0, 54, 12, 1, 0, 0, 0, 55, 56, 5, 43, 0, 0, 56, 14, 1, 0, 0, 0, 57, 58, 5, 115, 0, 0, 58, 59, 5, 117, 0, 0, 59, 60, 5, 109, 0, 0, 60, 16, 1, 0, 0, 0, 61, 62, 5, 115, 0, 0, 62, 63, 5, 117, 0, 0, 63, 64, 5, 109, 0, 0, 64, 65, 5, 95, 0, 0, 65, 66, 5, 99, 0, 0, 66, 67, 5, 111, 0, 0, 67, 68, 5, 110, 0, 0, 68, 69, 5, 110, 0, 0, 69, 70, 5, 101, 0, 0, 70, 71, 5, 99, 0, 0, 71, 72, 5, 116, 0, 0, 72, 73, 5, 105, 0, 0, 73, 74, 5, 111, 0, 0, 74, 75, 5, 110, 0, 0, 75, 76, 5, 115, 0, 0, 76, 18, 1, 0, 0, 0, 77, 78, 5, 46, 0, 0, 78, 79, 5, 46, 0, 0, 79, 20, 1, 0, 0, 0, 80, 81, 5, 44, 0, 0, 81, 22, 1, 0, 0, 0, 82, 83, 5, 91, 0, 0, 83, 24, 1, 0, 0, 0, 84, 85, 5, 93, 0, 0, 85, 26, 1, 0, 0, 0, 86, 87, 7, 0, 0, 0, 87, 28, 1, 0, 0, 0, 88, 89, 7, 1, 0, 0, 89, 30, 1, 0, 0, 0, 90, 93, 3, 29, 14, 0, 91, 93, 3, 27, 13, 0, 92, 90, 1, 0, 0, 0, 92, 91, 1, 0, 0, 0, 93, 32, 1, 0, 0, 0, 94, 96, 3, 27, 13, 0, 95, 94, 1, 0, 0, 0, 96, 97, 1, 0, 0, 0, 97, 95, 1, 0, 0, 0, 97, 98, 1, 0, 0, 0, 98, 105, 1, 0, 0, 0, 99, 101, 5, 46, 0, 0, 100, 102, 3, 27, 13, 0, 101, 100, 1, 0, 0, 0, 102, 103, 1, 0, 0, 0, 103, 101, 1, 0, 0, 0, 103, 104, 1, 0, 0, 0, 104, 106, 1, 0, 0, 0, 105, 99, 1, 0, 0, 0, 105, 106, 1, 0, 0, 0, 106, 34, 1, 0, 0, 0, 107, 108, 5, 116, 0, 0, 108, 36, 1, 0, 0, 0, 109, 113, 3, 29, 14, 0, 110, 112, 3, 31, 15, 0, 111, 110, 1, 0, 0, 0, 112, 115, 1, 0, 0, 0, 113, 111, 1, 0, 0, 0, 113, 114, 1, 0, 0, 0, 114, 38, 1, 0, 0, 0, 115, 113, 1, 0, 0, 0, 116, 122, 5, 61, 0, 0, 117, 118, 5, 62, 0, 0, 118, 122, 5, 61, 0, 0, 119, 120, 5, 60, 0, 0, 120, 122, 5, 61, 0, 0, 121, 116, 1, 0, 0, 0, 121, 117, 1, 0, 0, 0, 121, 119, 1, 0, 0, 0, 122, 40, 1, 0, 0, 0, 123, 124, 7, 2, 0, 0, 124, 125, 1, 0, 0, 0, 125, 126, 6, 20, 0, 0, 126, 42, 1, 0, 0, 0, 7, 0, 92, 97, 103, 105, 113, 121, 1, 6, 0, 0] \ No newline at end of file +[4, 0, 19, 131, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 14, 1, 14, 1, 15, 1, 15, 1, 16, 1, 16, 3, 16, 97, 8, 16, 1, 17, 4, 17, 100, 8, 17, 11, 17, 12, 17, 101, 1, 17, 1, 17, 4, 17, 106, 8, 17, 11, 17, 12, 17, 107, 3, 17, 110, 8, 17, 1, 18, 1, 18, 1, 19, 1, 19, 5, 19, 116, 8, 19, 10, 19, 12, 19, 119, 9, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 3, 20, 126, 8, 20, 1, 21, 1, 21, 1, 21, 1, 21, 0, 0, 22, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 0, 31, 0, 33, 0, 35, 15, 37, 16, 39, 17, 41, 18, 43, 19, 1, 0, 3, 1, 0, 48, 57, 3, 0, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 134, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 1, 45, 1, 0, 0, 0, 3, 47, 1, 0, 0, 0, 5, 49, 1, 0, 0, 0, 7, 51, 1, 0, 0, 0, 9, 53, 1, 0, 0, 0, 11, 55, 1, 0, 0, 0, 13, 57, 1, 0, 0, 0, 15, 59, 1, 0, 0, 0, 17, 61, 1, 0, 0, 0, 19, 65, 1, 0, 0, 0, 21, 81, 1, 0, 0, 0, 23, 84, 1, 0, 0, 0, 25, 86, 1, 0, 0, 0, 27, 88, 1, 0, 0, 0, 29, 90, 1, 0, 0, 0, 31, 92, 1, 0, 0, 0, 33, 96, 1, 0, 0, 0, 35, 99, 1, 0, 0, 0, 37, 111, 1, 0, 0, 0, 39, 113, 1, 0, 0, 0, 41, 125, 1, 0, 0, 0, 43, 127, 1, 0, 0, 0, 45, 46, 5, 46, 0, 0, 46, 2, 1, 0, 0, 0, 47, 48, 5, 94, 0, 0, 48, 4, 1, 0, 0, 0, 49, 50, 5, 45, 0, 0, 50, 6, 1, 0, 0, 0, 51, 52, 5, 40, 0, 0, 52, 8, 1, 0, 0, 0, 53, 54, 5, 41, 0, 0, 54, 10, 1, 0, 0, 0, 55, 56, 5, 47, 0, 0, 56, 12, 1, 0, 0, 0, 57, 58, 5, 42, 0, 0, 58, 14, 1, 0, 0, 0, 59, 60, 5, 43, 0, 0, 60, 16, 1, 0, 0, 0, 61, 62, 5, 115, 0, 0, 62, 63, 5, 117, 0, 0, 63, 64, 5, 109, 0, 0, 64, 18, 1, 0, 0, 0, 65, 66, 5, 115, 0, 0, 66, 67, 5, 117, 0, 0, 67, 68, 5, 109, 0, 0, 68, 69, 5, 95, 0, 0, 69, 70, 5, 99, 0, 0, 70, 71, 5, 111, 0, 0, 71, 72, 5, 110, 0, 0, 72, 73, 5, 110, 0, 0, 73, 74, 5, 101, 0, 0, 74, 75, 5, 99, 0, 0, 75, 76, 5, 116, 0, 0, 76, 77, 5, 105, 0, 0, 77, 78, 5, 111, 0, 0, 78, 79, 5, 110, 0, 0, 79, 80, 5, 115, 0, 0, 80, 20, 1, 0, 0, 0, 81, 82, 5, 46, 0, 0, 82, 83, 5, 46, 0, 0, 83, 22, 1, 0, 0, 0, 84, 85, 5, 44, 0, 0, 85, 24, 1, 0, 0, 0, 86, 87, 5, 91, 0, 0, 87, 26, 1, 0, 0, 0, 88, 89, 5, 93, 0, 0, 89, 28, 1, 0, 0, 0, 90, 91, 7, 0, 0, 0, 91, 30, 1, 0, 0, 0, 92, 93, 7, 1, 0, 0, 93, 32, 1, 0, 0, 0, 94, 97, 3, 31, 15, 0, 95, 97, 3, 29, 14, 0, 96, 94, 1, 0, 0, 0, 96, 95, 1, 0, 0, 0, 97, 34, 1, 0, 0, 0, 98, 100, 3, 29, 14, 0, 99, 98, 1, 0, 0, 0, 100, 101, 1, 0, 0, 0, 101, 99, 1, 0, 0, 0, 101, 102, 1, 0, 0, 0, 102, 109, 1, 0, 0, 0, 103, 105, 5, 46, 0, 0, 104, 106, 3, 29, 14, 0, 105, 104, 1, 0, 0, 0, 106, 107, 1, 0, 0, 0, 107, 105, 1, 0, 0, 0, 107, 108, 1, 0, 0, 0, 108, 110, 1, 0, 0, 0, 109, 103, 1, 0, 0, 0, 109, 110, 1, 0, 0, 0, 110, 36, 1, 0, 0, 0, 111, 112, 5, 116, 0, 0, 112, 38, 1, 0, 0, 0, 113, 117, 3, 31, 15, 0, 114, 116, 3, 33, 16, 0, 115, 114, 1, 0, 0, 0, 116, 119, 1, 0, 0, 0, 117, 115, 1, 0, 0, 0, 117, 118, 1, 0, 0, 0, 118, 40, 1, 0, 0, 0, 119, 117, 1, 0, 0, 0, 120, 126, 5, 61, 0, 0, 121, 122, 5, 62, 0, 0, 122, 126, 5, 61, 0, 0, 123, 124, 5, 60, 0, 0, 124, 126, 5, 61, 0, 0, 125, 120, 1, 0, 0, 0, 125, 121, 1, 0, 0, 0, 125, 123, 1, 0, 0, 0, 126, 42, 1, 0, 0, 0, 127, 128, 7, 2, 0, 0, 128, 129, 1, 0, 0, 0, 129, 130, 6, 21, 0, 0, 130, 44, 1, 0, 0, 0, 7, 0, 96, 101, 107, 109, 117, 125, 1, 6, 0, 0] \ No newline at end of file diff --git a/src/gems_craft/expression/parsing/antlr/ExprLexer.py b/src/gems_craft/expression/parsing/antlr/ExprLexer.py index 60c2d135..30cc405f 100644 --- a/src/gems_craft/expression/parsing/antlr/ExprLexer.py +++ b/src/gems_craft/expression/parsing/antlr/ExprLexer.py @@ -14,8 +14,8 @@ def serializedATN(): return [ 4, 0, - 18, - 127, + 19, + 131, 6, -1, 2, @@ -102,6 +102,10 @@ def serializedATN(): 20, 7, 20, + 2, + 21, + 7, + 21, 1, 0, 1, @@ -135,10 +139,6 @@ def serializedATN(): 1, 7, 1, - 7, - 1, - 7, - 1, 8, 1, 8, @@ -147,29 +147,31 @@ def serializedATN(): 1, 8, 1, - 8, + 9, 1, - 8, + 9, 1, - 8, + 9, 1, - 8, + 9, 1, - 8, + 9, 1, - 8, + 9, 1, - 8, + 9, 1, - 8, + 9, 1, - 8, + 9, 1, - 8, + 9, 1, - 8, + 9, 1, - 8, + 9, + 1, + 9, 1, 9, 1, @@ -181,6 +183,8 @@ def serializedATN(): 1, 10, 1, + 10, + 1, 11, 1, 11, @@ -200,76 +204,65 @@ def serializedATN(): 15, 1, 15, - 3, - 15, - 93, - 8, - 15, 1, 16, - 4, + 1, 16, - 96, + 3, + 16, + 97, 8, 16, + 1, + 17, + 4, + 17, + 100, + 8, + 17, 11, - 16, + 17, 12, - 16, - 97, + 17, + 101, 1, - 16, + 17, 1, - 16, + 17, 4, - 16, - 102, + 17, + 106, 8, - 16, + 17, 11, - 16, + 17, 12, - 16, - 103, + 17, + 107, 3, - 16, - 106, - 8, - 16, - 1, 17, - 1, + 110, + 8, 17, 1, 18, 1, 18, - 5, - 18, - 112, - 8, - 18, - 10, - 18, - 12, - 18, - 115, - 9, - 18, 1, 19, 1, 19, - 1, + 5, 19, - 1, + 116, + 8, 19, - 1, + 10, 19, - 3, + 12, 19, - 122, - 8, + 119, + 9, 19, 1, 20, @@ -279,9 +272,24 @@ def serializedATN(): 20, 1, 20, + 1, + 20, + 3, + 20, + 126, + 8, + 20, + 1, + 21, + 1, + 21, + 1, + 21, + 1, + 21, 0, 0, - 21, + 22, 1, 1, 3, @@ -309,13 +317,13 @@ def serializedATN(): 25, 13, 27, - 0, + 14, 29, 0, 31, 0, 33, - 14, + 0, 35, 15, 37, @@ -324,6 +332,8 @@ def serializedATN(): 17, 41, 18, + 43, + 19, 1, 0, 3, @@ -347,7 +357,7 @@ def serializedATN(): 13, 32, 32, - 130, + 134, 0, 1, 1, @@ -427,7 +437,7 @@ def serializedATN(): 0, 0, 0, - 33, + 27, 1, 0, 0, @@ -456,54 +466,60 @@ def serializedATN(): 0, 0, 0, - 1, + 0, 43, 1, 0, 0, 0, - 3, + 1, 45, 1, 0, 0, 0, - 5, + 3, 47, 1, 0, 0, 0, - 7, + 5, 49, 1, 0, 0, 0, - 9, + 7, 51, 1, 0, 0, 0, - 11, + 9, 53, 1, 0, 0, 0, - 13, + 11, 55, 1, 0, 0, 0, - 15, + 13, 57, 1, 0, 0, 0, + 15, + 59, + 1, + 0, + 0, + 0, 17, 61, 1, @@ -511,37 +527,37 @@ def serializedATN(): 0, 0, 19, - 77, + 65, 1, 0, 0, 0, 21, - 80, + 81, 1, 0, 0, 0, 23, - 82, + 84, 1, 0, 0, 0, 25, - 84, + 86, 1, 0, 0, 0, 27, - 86, + 88, 1, 0, 0, 0, 29, - 88, + 90, 1, 0, 0, @@ -553,43 +569,37 @@ def serializedATN(): 0, 0, 33, - 95, + 96, 1, 0, 0, 0, 35, - 107, + 99, 1, 0, 0, 0, 37, - 109, + 111, 1, 0, 0, 0, 39, - 121, + 113, 1, 0, 0, 0, 41, - 123, + 125, 1, 0, 0, 0, 43, - 44, - 5, - 46, - 0, - 0, - 44, - 2, + 127, 1, 0, 0, @@ -597,11 +607,11 @@ def serializedATN(): 45, 46, 5, - 45, + 46, 0, 0, 46, - 4, + 2, 1, 0, 0, @@ -609,11 +619,11 @@ def serializedATN(): 47, 48, 5, - 40, + 94, 0, 0, 48, - 6, + 4, 1, 0, 0, @@ -621,11 +631,11 @@ def serializedATN(): 49, 50, 5, - 41, + 45, 0, 0, 50, - 8, + 6, 1, 0, 0, @@ -633,11 +643,11 @@ def serializedATN(): 51, 52, 5, - 47, + 40, 0, 0, 52, - 10, + 8, 1, 0, 0, @@ -645,11 +655,11 @@ def serializedATN(): 53, 54, 5, - 42, + 41, 0, 0, 54, - 12, + 10, 1, 0, 0, @@ -657,11 +667,11 @@ def serializedATN(): 55, 56, 5, - 43, + 47, 0, 0, 56, - 14, + 12, 1, 0, 0, @@ -669,19 +679,19 @@ def serializedATN(): 57, 58, 5, - 115, + 42, 0, 0, 58, - 59, - 5, - 117, + 14, + 1, + 0, 0, 0, 59, 60, 5, - 109, + 43, 0, 0, 60, @@ -709,121 +719,121 @@ def serializedATN(): 0, 0, 64, - 65, - 5, - 95, + 18, + 1, + 0, 0, 0, 65, 66, 5, - 99, + 115, 0, 0, 66, 67, 5, - 111, + 117, 0, 0, 67, 68, 5, - 110, + 109, 0, 0, 68, 69, 5, - 110, + 95, 0, 0, 69, 70, 5, - 101, + 99, 0, 0, 70, 71, 5, - 99, + 111, 0, 0, 71, 72, 5, - 116, + 110, 0, 0, 72, 73, 5, - 105, + 110, 0, 0, 73, 74, 5, - 111, + 101, 0, 0, 74, 75, 5, - 110, + 99, 0, 0, 75, 76, 5, - 115, + 116, 0, 0, 76, - 18, - 1, - 0, + 77, + 5, + 105, 0, 0, 77, 78, 5, - 46, + 111, 0, 0, 78, 79, 5, - 46, + 110, 0, 0, 79, + 80, + 5, + 115, + 0, + 0, + 80, 20, 1, 0, 0, 0, - 80, 81, + 82, 5, - 44, - 0, - 0, - 81, - 22, - 1, - 0, + 46, 0, 0, 82, 83, 5, - 91, + 46, 0, 0, 83, - 24, + 22, 1, 0, 0, @@ -831,63 +841,57 @@ def serializedATN(): 84, 85, 5, - 93, + 44, 0, 0, 85, - 26, + 24, 1, 0, 0, 0, 86, 87, - 7, - 0, + 5, + 91, 0, 0, 87, - 28, + 26, 1, 0, 0, 0, 88, 89, - 7, - 1, + 5, + 93, 0, 0, 89, - 30, + 28, 1, 0, 0, 0, 90, - 93, - 3, - 29, - 14, - 0, 91, - 93, - 3, - 27, - 13, + 7, 0, - 92, - 90, + 0, + 0, + 91, + 30, 1, 0, 0, 0, 92, - 91, + 93, + 7, 1, 0, 0, - 0, 93, 32, 1, @@ -895,247 +899,277 @@ def serializedATN(): 0, 0, 94, - 96, + 97, 3, - 27, - 13, + 31, + 15, 0, 95, + 97, + 3, + 29, + 14, + 0, + 96, 94, 1, 0, 0, 0, 96, - 97, + 95, 1, 0, 0, 0, 97, - 95, + 34, 1, 0, 0, 0, - 97, + 98, + 100, + 3, + 29, + 14, + 0, + 99, 98, 1, 0, 0, 0, - 98, - 105, + 100, + 101, 1, 0, 0, 0, - 99, 101, - 5, - 46, + 99, + 1, 0, 0, - 100, - 102, - 3, - 27, - 13, 0, 101, - 100, + 102, 1, 0, 0, 0, 102, - 103, + 109, 1, 0, 0, 0, 103, - 101, - 1, + 105, + 5, + 46, 0, 0, + 104, + 106, + 3, + 29, + 14, 0, - 103, + 105, 104, 1, 0, 0, 0, - 104, 106, + 107, 1, 0, 0, 0, + 107, 105, - 99, 1, 0, 0, 0, - 105, - 106, + 107, + 108, 1, 0, 0, 0, - 106, - 34, + 108, + 110, 1, 0, 0, 0, - 107, - 108, - 5, - 116, + 109, + 103, + 1, 0, 0, - 108, + 0, + 109, + 110, + 1, + 0, + 0, + 0, + 110, 36, 1, 0, 0, 0, - 109, - 113, - 3, - 29, - 14, + 111, + 112, + 5, + 116, + 0, 0, - 110, 112, + 38, + 1, + 0, + 0, + 0, + 113, + 117, 3, 31, 15, 0, - 111, - 110, + 114, + 116, + 3, + 33, + 16, + 0, + 115, + 114, 1, 0, 0, 0, - 112, - 115, + 116, + 119, 1, 0, 0, 0, - 113, - 111, + 117, + 115, 1, 0, 0, 0, - 113, - 114, + 117, + 118, 1, 0, 0, 0, - 114, - 38, + 118, + 40, 1, 0, 0, 0, - 115, - 113, + 119, + 117, 1, 0, 0, 0, - 116, - 122, + 120, + 126, 5, 61, 0, 0, - 117, - 118, + 121, + 122, 5, 62, 0, 0, - 118, 122, + 126, 5, 61, 0, 0, - 119, - 120, + 123, + 124, 5, 60, 0, 0, - 120, - 122, + 124, + 126, 5, 61, 0, 0, - 121, - 116, + 125, + 120, 1, 0, 0, 0, + 125, 121, - 117, 1, 0, 0, 0, - 121, - 119, + 125, + 123, 1, 0, 0, 0, - 122, - 40, + 126, + 42, 1, 0, 0, 0, - 123, - 124, + 127, + 128, 7, 2, 0, 0, - 124, - 125, + 128, + 129, 1, 0, 0, 0, - 125, - 126, + 129, + 130, 6, - 20, + 21, 0, 0, - 126, - 42, + 130, + 44, 1, 0, 0, 0, 7, 0, - 92, - 97, - 103, - 105, - 113, - 121, + 96, + 101, + 107, + 109, + 117, + 125, 1, 6, 0, @@ -1144,6 +1178,7 @@ def serializedATN(): class ExprLexer(Lexer): + atn = ATNDeserializer().deserialize(serializedATN()) decisionsToDFA = [DFA(ds, i) for i, ds in enumerate(atn.decisionToState)] @@ -1161,11 +1196,12 @@ class ExprLexer(Lexer): T__10 = 11 T__11 = 12 T__12 = 13 - NUMBER = 14 - TIME = 15 - IDENTIFIER = 16 - COMPARISON = 17 - WS = 18 + T__13 = 14 + NUMBER = 15 + TIME = 16 + IDENTIFIER = 17 + COMPARISON = 18 + WS = 19 channelNames = ["DEFAULT_TOKEN_CHANNEL", "HIDDEN"] @@ -1174,6 +1210,7 @@ class ExprLexer(Lexer): literalNames = [ "", "'.'", + "'^'", "'-'", "'('", "')'", @@ -1205,6 +1242,7 @@ class ExprLexer(Lexer): "T__10", "T__11", "T__12", + "T__13", "DIGIT", "CHAR", "CHAR_OR_DIGIT", diff --git a/src/gems_craft/expression/parsing/antlr/ExprLexer.tokens b/src/gems_craft/expression/parsing/antlr/ExprLexer.tokens index c8638328..d1c7367f 100644 --- a/src/gems_craft/expression/parsing/antlr/ExprLexer.tokens +++ b/src/gems_craft/expression/parsing/antlr/ExprLexer.tokens @@ -11,22 +11,24 @@ T__9=10 T__10=11 T__11=12 T__12=13 -NUMBER=14 -TIME=15 -IDENTIFIER=16 -COMPARISON=17 -WS=18 +T__13=14 +NUMBER=15 +TIME=16 +IDENTIFIER=17 +COMPARISON=18 +WS=19 '.'=1 -'-'=2 -'('=3 -')'=4 -'/'=5 -'*'=6 -'+'=7 -'sum'=8 -'sum_connections'=9 -'..'=10 -','=11 -'['=12 -']'=13 -'t'=15 +'^'=2 +'-'=3 +'('=4 +')'=5 +'/'=6 +'*'=7 +'+'=8 +'sum'=9 +'sum_connections'=10 +'..'=11 +','=12 +'['=13 +']'=14 +'t'=16 diff --git a/src/gems_craft/expression/parsing/antlr/ExprParser.py b/src/gems_craft/expression/parsing/antlr/ExprParser.py index bdc68001..e3be8e15 100644 --- a/src/gems_craft/expression/parsing/antlr/ExprParser.py +++ b/src/gems_craft/expression/parsing/antlr/ExprParser.py @@ -15,8 +15,8 @@ def serializedATN(): return [ 4, 1, - 18, - 151, + 19, + 161, 2, 0, 7, @@ -49,6 +49,14 @@ def serializedATN(): 7, 7, 7, + 2, + 8, + 7, + 8, + 2, + 9, + 7, + 9, 1, 0, 1, @@ -127,7 +135,7 @@ def serializedATN(): 2, 3, 2, - 55, + 59, 8, 2, 1, @@ -182,7 +190,7 @@ def serializedATN(): 2, 3, 2, - 82, + 86, 8, 2, 1, @@ -203,16 +211,22 @@ def serializedATN(): 2, 1, 2, + 1, + 2, + 1, + 2, + 1, + 2, 5, 2, - 93, + 100, 8, 2, 10, 2, 12, 2, - 96, + 103, 9, 2, 1, @@ -223,14 +237,14 @@ def serializedATN(): 3, 5, 3, - 101, + 108, 8, 3, 10, 3, 12, 3, - 104, + 111, 9, 3, 1, @@ -239,7 +253,7 @@ def serializedATN(): 4, 3, 4, - 108, + 115, 8, 4, 1, @@ -248,7 +262,7 @@ def serializedATN(): 5, 3, 5, - 112, + 119, 8, 5, 1, @@ -267,33 +281,20 @@ def serializedATN(): 6, 1, 6, - 3, - 6, - 122, - 8, - 6, - 1, - 6, - 1, - 6, - 1, - 6, - 1, - 6, 1, 6, 1, 6, 5, 6, - 130, + 131, 8, 6, 10, 6, 12, 6, - 133, + 134, 9, 6, 1, @@ -308,37 +309,56 @@ def serializedATN(): 7, 1, 7, - 3, - 7, - 141, - 8, - 7, - 1, - 7, - 1, - 7, - 1, - 7, 5, 7, - 146, + 142, 8, 7, 10, 7, 12, 7, - 149, + 145, 9, 7, 1, - 7, + 8, + 1, + 8, + 1, + 8, + 1, + 8, + 1, + 8, + 3, + 8, + 152, + 8, + 8, + 1, + 9, + 1, + 9, + 1, + 9, + 1, + 9, + 1, + 9, + 3, + 9, + 159, + 8, + 9, + 1, + 9, 0, 3, 4, 12, 14, - 8, + 10, 0, 2, 4, @@ -347,383 +367,373 @@ def serializedATN(): 10, 12, 14, + 16, + 18, 0, 2, 1, 0, - 5, 6, + 7, 2, 0, - 2, - 2, - 7, - 7, - 165, + 3, + 3, + 8, + 8, + 174, 0, - 16, + 20, 1, 0, 0, 0, 2, - 20, + 24, 1, 0, 0, 0, 4, - 81, + 85, 1, 0, 0, 0, 6, - 97, + 104, 1, 0, 0, 0, 8, - 107, + 114, 1, 0, 0, 0, 10, - 109, + 116, 1, 0, 0, 0, 12, - 121, + 120, 1, 0, 0, 0, 14, - 140, + 135, 1, 0, 0, 0, 16, - 17, - 5, - 16, + 151, + 1, + 0, 0, 0, - 17, 18, + 158, + 1, + 0, + 0, + 0, + 20, + 21, + 5, + 17, + 0, + 0, + 21, + 22, 5, 1, 0, 0, - 18, - 19, + 22, + 23, 5, - 16, + 17, 0, 0, - 19, + 23, 1, 1, 0, 0, 0, - 20, - 21, + 24, + 25, 3, 4, 2, 0, - 21, - 22, + 25, + 26, 5, 0, 0, 1, - 22, + 26, 3, 1, 0, 0, 0, - 23, - 24, + 27, + 28, 6, 2, -1, 0, - 24, - 82, + 28, + 86, 3, 8, 4, 0, - 25, - 82, - 3, - 0, - 0, - 0, - 26, - 27, - 5, - 2, - 0, - 0, - 27, - 82, - 3, - 4, - 2, - 13, - 28, 29, - 5, + 86, 3, 0, 0, - 29, - 30, - 3, - 4, - 2, 0, 30, 31, 5, - 4, + 3, 0, 0, 31, - 82, - 1, - 0, - 0, - 0, + 86, + 3, + 4, + 2, + 13, 32, 33, 5, - 8, + 4, 0, 0, 33, 34, - 5, - 3, - 0, - 0, - 34, - 35, 3, 4, 2, 0, + 34, 35, - 36, 5, - 4, + 5, 0, 0, - 36, - 82, + 35, + 86, 1, 0, 0, 0, + 36, 37, - 38, 5, 9, 0, 0, + 37, 38, - 39, 5, - 3, + 4, 0, 0, + 38, 39, - 40, 3, + 4, + 2, 0, - 0, - 0, + 39, 40, - 41, 5, - 4, + 5, 0, 0, - 41, - 82, + 40, + 86, 1, 0, 0, 0, + 41, + 42, + 5, + 10, + 0, + 0, 42, 43, 5, - 8, + 4, 0, 0, 43, 44, - 5, 3, 0, 0, + 0, 44, 45, - 3, - 10, 5, + 5, + 0, 0, 45, - 46, - 5, - 10, + 86, + 1, + 0, 0, 0, 46, 47, - 3, - 10, 5, + 9, + 0, 0, 47, 48, 5, - 11, + 4, 0, 0, 48, 49, 3, - 4, - 2, + 10, + 5, 0, 49, 50, 5, - 4, + 11, 0, 0, 50, - 82, - 1, - 0, - 0, - 0, 51, - 52, + 3, + 10, 5, - 16, - 0, 0, + 51, 52, - 54, 5, - 3, + 12, 0, 0, + 52, 53, - 55, - 3, - 6, 3, + 4, + 2, 0, - 54, 53, - 1, - 0, + 54, + 5, + 5, 0, 0, 54, - 55, + 86, 1, 0, 0, 0, 55, 56, - 1, - 0, + 5, + 17, 0, 0, 56, - 82, + 58, 5, 4, 0, 0, 57, + 59, + 3, + 6, + 3, + 0, 58, - 5, - 16, + 57, + 1, + 0, 0, 0, 58, 59, - 5, - 12, + 1, + 0, 0, 0, 59, 60, - 3, - 10, - 5, + 1, + 0, + 0, 0, 60, - 61, + 86, + 5, 5, - 13, 0, 0, 61, - 82, - 1, - 0, + 62, + 5, + 17, 0, 0, 62, 63, 5, - 16, + 13, 0, 0, 63, 64, + 3, + 10, 5, - 12, - 0, 0, 64, 65, - 3, - 4, - 2, - 0, - 65, - 66, 5, - 13, + 14, 0, 0, - 66, - 82, + 65, + 86, 1, 0, 0, 0, + 66, + 67, + 5, + 17, + 0, + 0, 67, 68, 5, - 3, + 13, 0, 0, 68, @@ -735,320 +745,320 @@ def serializedATN(): 69, 70, 5, - 4, + 14, 0, 0, 70, - 71, - 5, - 12, + 86, + 1, + 0, 0, 0, 71, 72, - 3, - 10, 5, + 4, + 0, 0, 72, 73, - 5, - 13, - 0, + 3, + 4, + 2, 0, 73, - 82, - 1, - 0, + 74, + 5, + 5, 0, 0, 74, 75, 5, - 3, + 13, 0, 0, 75, 76, 3, - 4, - 2, + 10, + 5, 0, 76, 77, 5, - 4, + 14, 0, 0, 77, + 86, + 1, + 0, + 0, + 0, 78, + 79, 5, - 12, + 4, 0, 0, - 78, 79, + 80, 3, 4, 2, 0, - 79, 80, + 81, + 5, + 5, + 0, + 0, + 81, + 82, 5, 13, 0, 0, - 80, 82, - 1, + 83, + 3, + 4, + 2, 0, + 83, + 84, + 5, + 14, 0, 0, - 81, - 23, + 84, + 86, 1, 0, 0, 0, - 81, - 25, + 85, + 27, 1, 0, 0, 0, - 81, - 26, + 85, + 29, 1, 0, 0, 0, - 81, - 28, + 85, + 30, 1, 0, 0, 0, - 81, + 85, 32, 1, 0, 0, 0, - 81, - 37, + 85, + 36, 1, 0, 0, 0, - 81, - 42, + 85, + 41, 1, 0, 0, 0, - 81, - 51, + 85, + 46, 1, 0, 0, 0, - 81, - 57, + 85, + 55, 1, 0, 0, 0, - 81, - 62, + 85, + 61, 1, 0, 0, 0, - 81, - 67, + 85, + 66, 1, 0, 0, 0, - 81, - 74, + 85, + 71, 1, 0, 0, 0, - 82, - 94, + 85, + 78, 1, 0, 0, 0, - 83, - 84, - 10, - 11, - 0, - 0, - 84, - 85, - 7, + 86, + 101, + 1, 0, 0, 0, - 85, - 93, - 3, - 4, - 2, - 12, - 86, 87, + 88, 10, - 10, + 14, 0, 0, - 87, 88, - 7, - 1, + 89, + 5, + 2, 0, 0, - 88, - 93, + 89, + 100, 3, 4, 2, - 11, - 89, + 14, 90, + 91, 10, - 9, + 11, 0, 0, - 90, 91, - 5, - 17, + 92, + 7, 0, 0, - 91, - 93, + 0, + 92, + 100, 3, 4, 2, + 12, + 93, + 94, + 10, 10, - 92, - 83, - 1, - 0, - 0, - 0, - 92, - 86, - 1, - 0, 0, 0, - 92, - 89, + 94, + 95, + 7, 1, 0, 0, - 0, - 93, + 95, + 100, + 3, + 4, + 2, + 11, 96, - 1, + 97, + 10, + 9, 0, 0, + 97, + 98, + 5, + 18, 0, - 94, - 92, + 0, + 98, + 100, + 3, + 4, + 2, + 10, + 99, + 87, 1, 0, 0, 0, - 94, - 95, + 99, + 90, 1, 0, 0, 0, - 95, - 5, + 99, + 93, 1, 0, 0, 0, + 99, 96, - 94, 1, 0, 0, 0, - 97, - 102, - 3, - 4, - 2, - 0, - 98, - 99, - 5, - 11, - 0, - 0, - 99, - 101, - 3, - 4, - 2, - 0, 100, - 98, + 103, 1, 0, 0, 0, 101, - 104, + 99, 1, 0, 0, 0, + 101, 102, - 100, 1, 0, 0, 0, 102, - 103, + 5, 1, 0, 0, 0, 103, - 7, + 101, 1, 0, 0, 0, 104, - 102, - 1, - 0, - 0, + 109, + 3, + 4, + 2, 0, 105, - 108, + 106, 5, - 14, + 12, 0, 0, 106, 108, - 5, - 16, - 0, + 3, + 4, + 2, 0, 107, 105, @@ -1056,317 +1066,384 @@ def serializedATN(): 0, 0, 0, - 107, - 106, + 108, + 111, 1, 0, 0, 0, - 108, - 9, + 109, + 107, 1, 0, 0, 0, 109, - 111, - 5, - 15, + 110, + 1, 0, 0, - 110, - 112, - 3, - 12, - 6, 0, - 111, 110, + 7, 1, 0, 0, 0, 111, - 112, + 109, 1, 0, 0, 0, 112, - 11, - 1, - 0, + 115, + 5, + 15, 0, 0, 113, + 115, + 5, + 17, + 0, + 0, 114, - 6, - 6, - -1, + 112, + 1, + 0, + 0, 0, 114, - 115, - 7, + 113, 1, 0, 0, - 115, - 122, - 3, - 8, - 4, 0, - 116, - 117, - 7, + 115, + 9, 1, 0, 0, - 117, + 0, + 116, 118, 5, - 3, + 16, 0, 0, - 118, + 117, 119, 3, - 4, - 2, + 12, + 6, 0, - 119, - 120, - 5, - 4, + 118, + 117, + 1, 0, 0, - 120, - 122, + 0, + 118, + 119, 1, 0, 0, 0, - 121, - 113, + 119, + 11, 1, 0, 0, 0, + 120, 121, - 116, - 1, + 6, + 6, + -1, 0, + 121, + 122, + 7, + 1, 0, 0, 122, - 131, + 123, + 3, + 16, + 8, + 0, + 123, + 132, 1, 0, 0, 0, - 123, 124, + 125, 10, - 4, + 3, 0, 0, - 124, 125, + 126, 7, 0, 0, 0, - 125, - 130, + 126, + 131, 3, 14, 7, 0, - 126, 127, + 128, 10, - 3, + 2, 0, 0, - 127, 128, + 129, 7, 1, 0, 0, - 128, - 130, + 129, + 131, 3, 14, 7, 0, - 129, - 123, - 1, - 0, - 0, - 0, - 129, - 126, + 130, + 124, 1, 0, 0, 0, 130, - 133, + 127, 1, 0, 0, 0, 131, - 129, + 134, 1, 0, 0, 0, - 131, 132, + 130, 1, 0, 0, 0, 132, - 13, + 133, 1, 0, 0, 0, 133, - 131, + 13, 1, 0, 0, 0, 134, + 132, + 1, + 0, + 0, + 0, 135, + 136, 6, 7, -1, 0, - 135, - 136, - 5, - 3, - 0, - 0, 136, 137, 3, - 4, - 2, + 16, + 8, 0, 137, - 138, - 5, - 4, + 143, + 1, + 0, 0, 0, 138, + 139, + 10, + 2, + 0, + 0, + 139, + 140, + 7, + 0, + 0, + 0, + 140, + 142, + 3, + 14, + 7, + 3, 141, + 138, 1, 0, 0, 0, - 139, + 142, + 145, + 1, + 0, + 0, + 0, + 143, 141, + 1, + 0, + 0, + 0, + 143, + 144, + 1, + 0, + 0, + 0, + 144, + 15, + 1, + 0, + 0, + 0, + 145, + 143, + 1, + 0, + 0, + 0, + 146, + 147, 3, + 18, + 9, + 0, + 147, + 148, + 5, + 2, + 0, + 0, + 148, + 149, + 3, + 16, 8, - 4, 0, - 140, - 134, + 149, + 152, 1, 0, 0, 0, - 140, - 139, + 150, + 152, + 3, + 18, + 9, + 0, + 151, + 146, 1, 0, 0, 0, - 141, - 147, + 151, + 150, 1, 0, 0, 0, - 142, - 143, - 10, - 3, + 152, + 17, + 1, 0, 0, - 143, - 144, - 7, 0, + 153, + 154, + 5, + 4, 0, 0, - 144, - 146, + 154, + 155, 3, - 14, - 7, 4, - 145, - 142, - 1, + 2, 0, + 155, + 156, + 5, + 5, 0, 0, - 146, - 149, + 156, + 159, 1, 0, 0, 0, - 147, - 145, - 1, - 0, - 0, + 157, + 159, + 3, + 8, + 4, 0, - 147, - 148, + 158, + 153, 1, 0, 0, 0, - 148, - 15, + 158, + 157, 1, 0, 0, 0, - 149, - 147, + 159, + 19, 1, 0, 0, 0, 12, - 54, - 81, - 92, - 94, - 102, - 107, - 111, - 121, - 129, - 131, - 140, - 147, + 58, + 85, + 99, + 101, + 109, + 114, + 118, + 130, + 132, + 143, + 151, + 158, ] class ExprParser(Parser): + grammarFileName = "Expr.g4" atn = ATNDeserializer().deserialize(serializedATN()) @@ -1378,6 +1455,7 @@ class ExprParser(Parser): literalNames = [ "", "'.'", + "'^'", "'-'", "'('", "')'", @@ -1409,6 +1487,7 @@ class ExprParser(Parser): "", "", "", + "", "NUMBER", "TIME", "IDENTIFIER", @@ -1424,6 +1503,8 @@ class ExprParser(Parser): RULE_shift = 5 RULE_shift_expr = 6 RULE_right_expr = 7 + RULE_shift_operand = 8 + RULE_shift_primary = 9 ruleNames = [ "portFieldExpr", @@ -1434,6 +1515,8 @@ class ExprParser(Parser): "shift", "shift_expr", "right_expr", + "shift_operand", + "shift_primary", ] EOF = Token.EOF @@ -1450,11 +1533,12 @@ class ExprParser(Parser): T__10 = 11 T__11 = 12 T__12 = 13 - NUMBER = 14 - TIME = 15 - IDENTIFIER = 16 - COMPARISON = 17 - WS = 18 + T__13 = 14 + NUMBER = 15 + TIME = 16 + IDENTIFIER = 17 + COMPARISON = 18 + WS = 19 def __init__(self, input: TokenStream, output: TextIO = sys.stdout): super().__init__(input, output) @@ -1489,15 +1573,16 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) def portFieldExpr(self): + localctx = ExprParser.PortFieldExprContext(self, self._ctx, self.state) self.enterRule(localctx, 0, self.RULE_portFieldExpr) try: self.enterOuterAlt(localctx, 1) - self.state = 16 + self.state = 20 self.match(ExprParser.IDENTIFIER) - self.state = 17 + self.state = 21 self.match(ExprParser.T__0) - self.state = 18 + self.state = 22 self.match(ExprParser.IDENTIFIER) except RecognitionException as re: localctx.exception = re @@ -1532,13 +1617,14 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) def fullexpr(self): + localctx = ExprParser.FullexprContext(self, self._ctx, self.state) self.enterRule(localctx, 2, self.RULE_fullexpr) try: self.enterOuterAlt(localctx, 1) - self.state = 20 + self.state = 24 self.expr(0) - self.state = 21 + self.state = 25 self.match(ExprParser.EOF) except RecognitionException as re: localctx.exception = re @@ -1564,6 +1650,7 @@ def copyFrom(self, ctx: ParserRuleContext): super().copyFrom(ctx) class PortFieldSumContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1580,6 +1667,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class NegationContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1596,6 +1684,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class UnsignedAtomContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1612,6 +1701,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class ExpressionContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1628,6 +1718,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class ComparisonContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1650,6 +1741,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class AllTimeSumContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1666,6 +1758,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class TimeIndexExprContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1685,6 +1778,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class AddsubContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1705,6 +1799,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class TimeShiftExprContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1724,6 +1819,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class PortFieldContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1740,6 +1836,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class MuldivContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1760,6 +1857,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class TimeSumContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1784,6 +1882,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class TimeIndexContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1803,6 +1902,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class TimeShiftContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1822,6 +1922,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class FunctionContext(ExprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.ExprContext @@ -1840,6 +1941,26 @@ def accept(self, visitor: ParseTreeVisitor): else: return visitor.visitChildren(self) + class PowerContext(ExprContext): + + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def expr(self, i: int = None): + if i is None: + return self.getTypedRuleContexts(ExprParser.ExprContext) + else: + return self.getTypedRuleContext(ExprParser.ExprContext, i) + + def accept(self, visitor: ParseTreeVisitor): + if hasattr(visitor, "visitPower"): + return visitor.visitPower(self) + else: + return visitor.visitChildren(self) + def expr(self, _p: int = 0): _parentctx = self._ctx _parentState = self.state @@ -1850,7 +1971,7 @@ def expr(self, _p: int = 0): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 81 + self.state = 85 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input, 1, self._ctx) if la_ == 1: @@ -1858,7 +1979,7 @@ def expr(self, _p: int = 0): self._ctx = localctx _prevctx = localctx - self.state = 24 + self.state = 28 self.atom() pass @@ -1866,7 +1987,7 @@ def expr(self, _p: int = 0): localctx = ExprParser.PortFieldContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 25 + self.state = 29 self.portFieldExpr() pass @@ -1874,9 +1995,9 @@ def expr(self, _p: int = 0): localctx = ExprParser.NegationContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 26 - self.match(ExprParser.T__1) - self.state = 27 + self.state = 30 + self.match(ExprParser.T__2) + self.state = 31 self.expr(13) pass @@ -1884,149 +2005,149 @@ def expr(self, _p: int = 0): localctx = ExprParser.ExpressionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 28 - self.match(ExprParser.T__2) - self.state = 29 - self.expr(0) - self.state = 30 + self.state = 32 self.match(ExprParser.T__3) + self.state = 33 + self.expr(0) + self.state = 34 + self.match(ExprParser.T__4) pass elif la_ == 5: localctx = ExprParser.AllTimeSumContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 32 - self.match(ExprParser.T__7) - self.state = 33 - self.match(ExprParser.T__2) - self.state = 34 - self.expr(0) - self.state = 35 + self.state = 36 + self.match(ExprParser.T__8) + self.state = 37 self.match(ExprParser.T__3) + self.state = 38 + self.expr(0) + self.state = 39 + self.match(ExprParser.T__4) pass elif la_ == 6: localctx = ExprParser.PortFieldSumContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 37 - self.match(ExprParser.T__8) - self.state = 38 - self.match(ExprParser.T__2) - self.state = 39 - self.portFieldExpr() - self.state = 40 + self.state = 41 + self.match(ExprParser.T__9) + self.state = 42 self.match(ExprParser.T__3) + self.state = 43 + self.portFieldExpr() + self.state = 44 + self.match(ExprParser.T__4) pass elif la_ == 7: localctx = ExprParser.TimeSumContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 42 - self.match(ExprParser.T__7) - self.state = 43 - self.match(ExprParser.T__2) - self.state = 44 - localctx.from_ = self.shift() - self.state = 45 - self.match(ExprParser.T__9) self.state = 46 - localctx.to = self.shift() + self.match(ExprParser.T__8) self.state = 47 - self.match(ExprParser.T__10) + self.match(ExprParser.T__3) self.state = 48 - self.expr(0) + localctx.from_ = self.shift() self.state = 49 - self.match(ExprParser.T__3) + self.match(ExprParser.T__10) + self.state = 50 + localctx.to = self.shift() + self.state = 51 + self.match(ExprParser.T__11) + self.state = 52 + self.expr(0) + self.state = 53 + self.match(ExprParser.T__4) pass elif la_ == 8: localctx = ExprParser.FunctionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 51 + self.state = 55 self.match(ExprParser.IDENTIFIER) - self.state = 52 - self.match(ExprParser.T__2) - self.state = 54 + self.state = 56 + self.match(ExprParser.T__3) + self.state = 58 self._errHandler.sync(self) _la = self._input.LA(1) - if ((_la) & ~0x3F) == 0 and ((1 << _la) & 82700) != 0: - self.state = 53 + if ((_la) & ~0x3F) == 0 and ((1 << _la) & 165400) != 0: + self.state = 57 self.argList() - self.state = 56 - self.match(ExprParser.T__3) + self.state = 60 + self.match(ExprParser.T__4) pass elif la_ == 9: localctx = ExprParser.TimeShiftContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 57 + self.state = 61 self.match(ExprParser.IDENTIFIER) - self.state = 58 - self.match(ExprParser.T__11) - self.state = 59 - self.shift() - self.state = 60 + self.state = 62 self.match(ExprParser.T__12) + self.state = 63 + self.shift() + self.state = 64 + self.match(ExprParser.T__13) pass elif la_ == 10: localctx = ExprParser.TimeIndexContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 62 + self.state = 66 self.match(ExprParser.IDENTIFIER) - self.state = 63 - self.match(ExprParser.T__11) - self.state = 64 - self.expr(0) - self.state = 65 + self.state = 67 self.match(ExprParser.T__12) + self.state = 68 + self.expr(0) + self.state = 69 + self.match(ExprParser.T__13) pass elif la_ == 11: localctx = ExprParser.TimeShiftExprContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 67 - self.match(ExprParser.T__2) - self.state = 68 - self.expr(0) - self.state = 69 - self.match(ExprParser.T__3) - self.state = 70 - self.match(ExprParser.T__11) self.state = 71 - self.shift() + self.match(ExprParser.T__3) self.state = 72 + self.expr(0) + self.state = 73 + self.match(ExprParser.T__4) + self.state = 74 self.match(ExprParser.T__12) + self.state = 75 + self.shift() + self.state = 76 + self.match(ExprParser.T__13) pass elif la_ == 12: localctx = ExprParser.TimeIndexExprContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 74 - self.match(ExprParser.T__2) - self.state = 75 - self.expr(0) - self.state = 76 - self.match(ExprParser.T__3) - self.state = 77 - self.match(ExprParser.T__11) self.state = 78 - self.expr(0) + self.match(ExprParser.T__3) self.state = 79 + self.expr(0) + self.state = 80 + self.match(ExprParser.T__4) + self.state = 81 self.match(ExprParser.T__12) + self.state = 82 + self.expr(0) + self.state = 83 + self.match(ExprParser.T__13) pass self._ctx.stop = self._input.LT(-1) - self.state = 94 + self.state = 101 self._errHandler.sync(self) _alt = self._interp.adaptivePredict(self._input, 3, self._ctx) while _alt != 2 and _alt != ATN.INVALID_ALT_NUMBER: @@ -2034,82 +2155,102 @@ def expr(self, _p: int = 0): if self._parseListeners is not None: self.triggerExitRuleEvent() _prevctx = localctx - self.state = 92 + self.state = 99 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input, 2, self._ctx) if la_ == 1: + localctx = ExprParser.PowerContext( + self, ExprParser.ExprContext(self, _parentctx, _parentState) + ) + self.pushNewRecursionContext( + localctx, _startState, self.RULE_expr + ) + self.state = 87 + if not self.precpred(self._ctx, 14): + from antlr4.error.Errors import FailedPredicateException + + raise FailedPredicateException( + self, "self.precpred(self._ctx, 14)" + ) + self.state = 88 + self.match(ExprParser.T__1) + self.state = 89 + self.expr(14) + pass + + elif la_ == 2: localctx = ExprParser.MuldivContext( self, ExprParser.ExprContext(self, _parentctx, _parentState) ) self.pushNewRecursionContext( localctx, _startState, self.RULE_expr ) - self.state = 83 + self.state = 90 if not self.precpred(self._ctx, 11): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( self, "self.precpred(self._ctx, 11)" ) - self.state = 84 + self.state = 91 localctx.op = self._input.LT(1) _la = self._input.LA(1) - if not (_la == 5 or _la == 6): + if not (_la == 6 or _la == 7): localctx.op = self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 85 + self.state = 92 self.expr(12) pass - elif la_ == 2: + elif la_ == 3: localctx = ExprParser.AddsubContext( self, ExprParser.ExprContext(self, _parentctx, _parentState) ) self.pushNewRecursionContext( localctx, _startState, self.RULE_expr ) - self.state = 86 + self.state = 93 if not self.precpred(self._ctx, 10): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( self, "self.precpred(self._ctx, 10)" ) - self.state = 87 + self.state = 94 localctx.op = self._input.LT(1) _la = self._input.LA(1) - if not (_la == 2 or _la == 7): + if not (_la == 3 or _la == 8): localctx.op = self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 88 + self.state = 95 self.expr(11) pass - elif la_ == 3: + elif la_ == 4: localctx = ExprParser.ComparisonContext( self, ExprParser.ExprContext(self, _parentctx, _parentState) ) self.pushNewRecursionContext( localctx, _startState, self.RULE_expr ) - self.state = 89 + self.state = 96 if not self.precpred(self._ctx, 9): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( self, "self.precpred(self._ctx, 9)" ) - self.state = 90 + self.state = 97 self.match(ExprParser.COMPARISON) - self.state = 91 + self.state = 98 self.expr(10) pass - self.state = 96 + self.state = 103 self._errHandler.sync(self) _alt = self._interp.adaptivePredict(self._input, 3, self._ctx) @@ -2146,22 +2287,23 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) def argList(self): + localctx = ExprParser.ArgListContext(self, self._ctx, self.state) self.enterRule(localctx, 6, self.RULE_argList) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 97 + self.state = 104 self.expr(0) - self.state = 102 + self.state = 109 self._errHandler.sync(self) _la = self._input.LA(1) - while _la == 11: - self.state = 98 - self.match(ExprParser.T__10) - self.state = 99 + while _la == 12: + self.state = 105 + self.match(ExprParser.T__11) + self.state = 106 self.expr(0) - self.state = 104 + self.state = 111 self._errHandler.sync(self) _la = self._input.LA(1) @@ -2189,6 +2331,7 @@ def copyFrom(self, ctx: ParserRuleContext): super().copyFrom(ctx) class NumberContext(AtomContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.AtomContext @@ -2205,6 +2348,7 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) class IdentifierContext(AtomContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.AtomContext @@ -2221,22 +2365,23 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) def atom(self): + localctx = ExprParser.AtomContext(self, self._ctx, self.state) self.enterRule(localctx, 8, self.RULE_atom) try: - self.state = 107 + self.state = 114 self._errHandler.sync(self) token = self._input.LA(1) - if token in [14]: + if token in [15]: localctx = ExprParser.NumberContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 105 + self.state = 112 self.match(ExprParser.NUMBER) pass - elif token in [16]: + elif token in [17]: localctx = ExprParser.IdentifierContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 106 + self.state = 113 self.match(ExprParser.IDENTIFIER) pass else: @@ -2275,18 +2420,19 @@ def accept(self, visitor: ParseTreeVisitor): return visitor.visitChildren(self) def shift(self): + localctx = ExprParser.ShiftContext(self, self._ctx, self.state) self.enterRule(localctx, 10, self.RULE_shift) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 109 + self.state = 116 self.match(ExprParser.TIME) - self.state = 111 + self.state = 118 self._errHandler.sync(self) _la = self._input.LA(1) - if _la == 2 or _la == 7: - self.state = 110 + if _la == 3 or _la == 8: + self.state = 117 self.shift_expr(0) except RecognitionException as re: @@ -2312,24 +2458,8 @@ def getRuleIndex(self): def copyFrom(self, ctx: ParserRuleContext): super().copyFrom(ctx) - class SignedAtomContext(Shift_exprContext): - def __init__( - self, parser, ctx: ParserRuleContext - ): # actually a ExprParser.Shift_exprContext - super().__init__(parser) - self.op = None # Token - self.copyFrom(ctx) - - def atom(self): - return self.getTypedRuleContext(ExprParser.AtomContext, 0) - - def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitSignedAtom"): - return visitor.visitSignedAtom(self) - else: - return visitor.visitChildren(self) + class ShiftMuldivContext(Shift_exprContext): - class SignedExpressionContext(Shift_exprContext): def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.Shift_exprContext @@ -2337,16 +2467,20 @@ def __init__( self.op = None # Token self.copyFrom(ctx) - def expr(self): - return self.getTypedRuleContext(ExprParser.ExprContext, 0) + def shift_expr(self): + return self.getTypedRuleContext(ExprParser.Shift_exprContext, 0) + + def right_expr(self): + return self.getTypedRuleContext(ExprParser.Right_exprContext, 0) def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitSignedExpression"): - return visitor.visitSignedExpression(self) + if hasattr(visitor, "visitShiftMuldiv"): + return visitor.visitShiftMuldiv(self) else: return visitor.visitChildren(self) - class ShiftMuldivContext(Shift_exprContext): + class ShiftAddsubContext(Shift_exprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.Shift_exprContext @@ -2361,12 +2495,13 @@ def right_expr(self): return self.getTypedRuleContext(ExprParser.Right_exprContext, 0) def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitShiftMuldiv"): - return visitor.visitShiftMuldiv(self) + if hasattr(visitor, "visitShiftAddsub"): + return visitor.visitShiftAddsub(self) else: return visitor.visitChildren(self) - class ShiftAddsubContext(Shift_exprContext): + class SignedOperandContext(Shift_exprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.Shift_exprContext @@ -2374,15 +2509,12 @@ def __init__( self.op = None # Token self.copyFrom(ctx) - def shift_expr(self): - return self.getTypedRuleContext(ExprParser.Shift_exprContext, 0) - - def right_expr(self): - return self.getTypedRuleContext(ExprParser.Right_exprContext, 0) + def shift_operand(self): + return self.getTypedRuleContext(ExprParser.Shift_operandContext, 0) def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitShiftAddsub"): - return visitor.visitShiftAddsub(self) + if hasattr(visitor, "visitSignedOperand"): + return visitor.visitSignedOperand(self) else: return visitor.visitChildren(self) @@ -2396,58 +2528,32 @@ def shift_expr(self, _p: int = 0): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 121 - self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input, 7, self._ctx) - if la_ == 1: - localctx = ExprParser.SignedAtomContext(self, localctx) - self._ctx = localctx - _prevctx = localctx - - self.state = 114 - localctx.op = self._input.LT(1) - _la = self._input.LA(1) - if not (_la == 2 or _la == 7): - localctx.op = self._errHandler.recoverInline(self) - else: - self._errHandler.reportMatch(self) - self.consume() - self.state = 115 - self.atom() - pass - - elif la_ == 2: - localctx = ExprParser.SignedExpressionContext(self, localctx) - self._ctx = localctx - _prevctx = localctx - self.state = 116 - localctx.op = self._input.LT(1) - _la = self._input.LA(1) - if not (_la == 2 or _la == 7): - localctx.op = self._errHandler.recoverInline(self) - else: - self._errHandler.reportMatch(self) - self.consume() - self.state = 117 - self.match(ExprParser.T__2) - self.state = 118 - self.expr(0) - self.state = 119 - self.match(ExprParser.T__3) - pass + localctx = ExprParser.SignedOperandContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 121 + localctx.op = self._input.LT(1) + _la = self._input.LA(1) + if not (_la == 3 or _la == 8): + localctx.op = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 122 + self.shift_operand() self._ctx.stop = self._input.LT(-1) - self.state = 131 + self.state = 132 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input, 9, self._ctx) + _alt = self._interp.adaptivePredict(self._input, 8, self._ctx) while _alt != 2 and _alt != ATN.INVALID_ALT_NUMBER: if _alt == 1: if self._parseListeners is not None: self.triggerExitRuleEvent() _prevctx = localctx - self.state = 129 + self.state = 130 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input, 8, self._ctx) + la_ = self._interp.adaptivePredict(self._input, 7, self._ctx) if la_ == 1: localctx = ExprParser.ShiftMuldivContext( self, @@ -2458,22 +2564,22 @@ def shift_expr(self, _p: int = 0): self.pushNewRecursionContext( localctx, _startState, self.RULE_shift_expr ) - self.state = 123 - if not self.precpred(self._ctx, 4): + self.state = 124 + if not self.precpred(self._ctx, 3): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( - self, "self.precpred(self._ctx, 4)" + self, "self.precpred(self._ctx, 3)" ) - self.state = 124 + self.state = 125 localctx.op = self._input.LT(1) _la = self._input.LA(1) - if not (_la == 5 or _la == 6): + if not (_la == 6 or _la == 7): localctx.op = self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 125 + self.state = 126 self.right_expr(0) pass @@ -2487,28 +2593,28 @@ def shift_expr(self, _p: int = 0): self.pushNewRecursionContext( localctx, _startState, self.RULE_shift_expr ) - self.state = 126 - if not self.precpred(self._ctx, 3): + self.state = 127 + if not self.precpred(self._ctx, 2): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( - self, "self.precpred(self._ctx, 3)" + self, "self.precpred(self._ctx, 2)" ) - self.state = 127 + self.state = 128 localctx.op = self._input.LT(1) _la = self._input.LA(1) - if not (_la == 2 or _la == 7): + if not (_la == 3 or _la == 8): localctx.op = self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 128 + self.state = 129 self.right_expr(0) pass - self.state = 133 + self.state = 134 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input, 9, self._ctx) + _alt = self._interp.adaptivePredict(self._input, 8, self._ctx) except RecognitionException as re: localctx.exception = re @@ -2533,23 +2639,25 @@ def getRuleIndex(self): def copyFrom(self, ctx: ParserRuleContext): super().copyFrom(ctx) - class RightExpressionContext(Right_exprContext): + class RightOperandContext(Right_exprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.Right_exprContext super().__init__(parser) self.copyFrom(ctx) - def expr(self): - return self.getTypedRuleContext(ExprParser.ExprContext, 0) + def shift_operand(self): + return self.getTypedRuleContext(ExprParser.Shift_operandContext, 0) def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitRightExpression"): - return visitor.visitRightExpression(self) + if hasattr(visitor, "visitRightOperand"): + return visitor.visitRightOperand(self) else: return visitor.visitChildren(self) class RightMuldivContext(Right_exprContext): + def __init__( self, parser, ctx: ParserRuleContext ): # actually a ExprParser.Right_exprContext @@ -2569,22 +2677,6 @@ def accept(self, visitor: ParseTreeVisitor): else: return visitor.visitChildren(self) - class RightAtomContext(Right_exprContext): - def __init__( - self, parser, ctx: ParserRuleContext - ): # actually a ExprParser.Right_exprContext - super().__init__(parser) - self.copyFrom(ctx) - - def atom(self): - return self.getTypedRuleContext(ExprParser.AtomContext, 0) - - def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitRightAtom"): - return visitor.visitRightAtom(self) - else: - return visitor.visitChildren(self) - def right_expr(self, _p: int = 0): _parentctx = self._ctx _parentState = self.state @@ -2595,35 +2687,16 @@ def right_expr(self, _p: int = 0): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 140 - self._errHandler.sync(self) - token = self._input.LA(1) - if token in [3]: - localctx = ExprParser.RightExpressionContext(self, localctx) - self._ctx = localctx - _prevctx = localctx - - self.state = 135 - self.match(ExprParser.T__2) - self.state = 136 - self.expr(0) - self.state = 137 - self.match(ExprParser.T__3) - pass - elif token in [14, 16]: - localctx = ExprParser.RightAtomContext(self, localctx) - self._ctx = localctx - _prevctx = localctx - self.state = 139 - self.atom() - pass - else: - raise NoViableAltException(self) + localctx = ExprParser.RightOperandContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 136 + self.shift_operand() self._ctx.stop = self._input.LT(-1) - self.state = 147 + self.state = 143 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input, 11, self._ctx) + _alt = self._interp.adaptivePredict(self._input, 9, self._ctx) while _alt != 2 and _alt != ATN.INVALID_ALT_NUMBER: if _alt == 1: if self._parseListeners is not None: @@ -2636,26 +2709,26 @@ def right_expr(self, _p: int = 0): self.pushNewRecursionContext( localctx, _startState, self.RULE_right_expr ) - self.state = 142 - if not self.precpred(self._ctx, 3): + self.state = 138 + if not self.precpred(self._ctx, 2): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( - self, "self.precpred(self._ctx, 3)" + self, "self.precpred(self._ctx, 2)" ) - self.state = 143 + self.state = 139 localctx.op = self._input.LT(1) _la = self._input.LA(1) - if not (_la == 5 or _la == 6): + if not (_la == 6 or _la == 7): localctx.op = self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 144 - self.right_expr(4) - self.state = 149 + self.state = 140 + self.right_expr(3) + self.state = 145 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input, 11, self._ctx) + _alt = self._interp.adaptivePredict(self._input, 9, self._ctx) except RecognitionException as re: localctx.exception = re @@ -2665,6 +2738,176 @@ def right_expr(self, _p: int = 0): self.unrollRecursionContexts(_parentctx) return localctx + class Shift_operandContext(ParserRuleContext): + __slots__ = "parser" + + def __init__( + self, parser, parent: ParserRuleContext = None, invokingState: int = -1 + ): + super().__init__(parent, invokingState) + self.parser = parser + + def getRuleIndex(self): + return ExprParser.RULE_shift_operand + + def copyFrom(self, ctx: ParserRuleContext): + super().copyFrom(ctx) + + class RightPrimaryContext(Shift_operandContext): + + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.Shift_operandContext + super().__init__(parser) + self.copyFrom(ctx) + + def shift_primary(self): + return self.getTypedRuleContext(ExprParser.Shift_primaryContext, 0) + + def accept(self, visitor: ParseTreeVisitor): + if hasattr(visitor, "visitRightPrimary"): + return visitor.visitRightPrimary(self) + else: + return visitor.visitChildren(self) + + class RightPowerContext(Shift_operandContext): + + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.Shift_operandContext + super().__init__(parser) + self.copyFrom(ctx) + + def shift_primary(self): + return self.getTypedRuleContext(ExprParser.Shift_primaryContext, 0) + + def shift_operand(self): + return self.getTypedRuleContext(ExprParser.Shift_operandContext, 0) + + def accept(self, visitor: ParseTreeVisitor): + if hasattr(visitor, "visitRightPower"): + return visitor.visitRightPower(self) + else: + return visitor.visitChildren(self) + + def shift_operand(self): + + localctx = ExprParser.Shift_operandContext(self, self._ctx, self.state) + self.enterRule(localctx, 16, self.RULE_shift_operand) + try: + self.state = 151 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input, 10, self._ctx) + if la_ == 1: + localctx = ExprParser.RightPowerContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 146 + self.shift_primary() + self.state = 147 + self.match(ExprParser.T__1) + self.state = 148 + self.shift_operand() + pass + + elif la_ == 2: + localctx = ExprParser.RightPrimaryContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 150 + self.shift_primary() + pass + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class Shift_primaryContext(ParserRuleContext): + __slots__ = "parser" + + def __init__( + self, parser, parent: ParserRuleContext = None, invokingState: int = -1 + ): + super().__init__(parent, invokingState) + self.parser = parser + + def getRuleIndex(self): + return ExprParser.RULE_shift_primary + + def copyFrom(self, ctx: ParserRuleContext): + super().copyFrom(ctx) + + class RightExpressionContext(Shift_primaryContext): + + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.Shift_primaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def expr(self): + return self.getTypedRuleContext(ExprParser.ExprContext, 0) + + def accept(self, visitor: ParseTreeVisitor): + if hasattr(visitor, "visitRightExpression"): + return visitor.visitRightExpression(self) + else: + return visitor.visitChildren(self) + + class RightAtomContext(Shift_primaryContext): + + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.Shift_primaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def atom(self): + return self.getTypedRuleContext(ExprParser.AtomContext, 0) + + def accept(self, visitor: ParseTreeVisitor): + if hasattr(visitor, "visitRightAtom"): + return visitor.visitRightAtom(self) + else: + return visitor.visitChildren(self) + + def shift_primary(self): + + localctx = ExprParser.Shift_primaryContext(self, self._ctx, self.state) + self.enterRule(localctx, 18, self.RULE_shift_primary) + try: + self.state = 158 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [4]: + localctx = ExprParser.RightExpressionContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 153 + self.match(ExprParser.T__3) + self.state = 154 + self.expr(0) + self.state = 155 + self.match(ExprParser.T__4) + pass + elif token in [15, 17]: + localctx = ExprParser.RightAtomContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 157 + self.atom() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + def sempred(self, localctx: RuleContext, ruleIndex: int, predIndex: int): if self._predicates == None: self._predicates = dict() @@ -2679,21 +2922,24 @@ def sempred(self, localctx: RuleContext, ruleIndex: int, predIndex: int): def expr_sempred(self, localctx: ExprContext, predIndex: int): if predIndex == 0: - return self.precpred(self._ctx, 11) + return self.precpred(self._ctx, 14) if predIndex == 1: - return self.precpred(self._ctx, 10) + return self.precpred(self._ctx, 11) if predIndex == 2: - return self.precpred(self._ctx, 9) + return self.precpred(self._ctx, 10) - def shift_expr_sempred(self, localctx: Shift_exprContext, predIndex: int): if predIndex == 3: - return self.precpred(self._ctx, 4) + return self.precpred(self._ctx, 9) + def shift_expr_sempred(self, localctx: Shift_exprContext, predIndex: int): if predIndex == 4: return self.precpred(self._ctx, 3) - def right_expr_sempred(self, localctx: Right_exprContext, predIndex: int): if predIndex == 5: - return self.precpred(self._ctx, 3) + return self.precpred(self._ctx, 2) + + def right_expr_sempred(self, localctx: Right_exprContext, predIndex: int): + if predIndex == 6: + return self.precpred(self._ctx, 2) diff --git a/src/gems_craft/expression/parsing/antlr/ExprVisitor.py b/src/gems_craft/expression/parsing/antlr/ExprVisitor.py index 3a4cf8d6..ca09b432 100644 --- a/src/gems_craft/expression/parsing/antlr/ExprVisitor.py +++ b/src/gems_craft/expression/parsing/antlr/ExprVisitor.py @@ -10,6 +10,7 @@ class ExprVisitor(ParseTreeVisitor): + # Visit a parse tree produced by ExprParser#portFieldExpr. def visitPortFieldExpr(self, ctx: ExprParser.PortFieldExprContext): return self.visitChildren(ctx) @@ -78,6 +79,10 @@ def visitTimeShift(self, ctx: ExprParser.TimeShiftContext): def visitFunction(self, ctx: ExprParser.FunctionContext): return self.visitChildren(ctx) + # Visit a parse tree produced by ExprParser#power. + def visitPower(self, ctx: ExprParser.PowerContext): + return self.visitChildren(ctx) + # Visit a parse tree produced by ExprParser#argList. def visitArgList(self, ctx: ExprParser.ArgListContext): return self.visitChildren(ctx) @@ -94,14 +99,6 @@ def visitIdentifier(self, ctx: ExprParser.IdentifierContext): def visitShift(self, ctx: ExprParser.ShiftContext): return self.visitChildren(ctx) - # Visit a parse tree produced by ExprParser#signedAtom. - def visitSignedAtom(self, ctx: ExprParser.SignedAtomContext): - return self.visitChildren(ctx) - - # Visit a parse tree produced by ExprParser#signedExpression. - def visitSignedExpression(self, ctx: ExprParser.SignedExpressionContext): - return self.visitChildren(ctx) - # Visit a parse tree produced by ExprParser#shiftMuldiv. def visitShiftMuldiv(self, ctx: ExprParser.ShiftMuldivContext): return self.visitChildren(ctx) @@ -110,14 +107,30 @@ def visitShiftMuldiv(self, ctx: ExprParser.ShiftMuldivContext): def visitShiftAddsub(self, ctx: ExprParser.ShiftAddsubContext): return self.visitChildren(ctx) - # Visit a parse tree produced by ExprParser#rightExpression. - def visitRightExpression(self, ctx: ExprParser.RightExpressionContext): + # Visit a parse tree produced by ExprParser#signedOperand. + def visitSignedOperand(self, ctx: ExprParser.SignedOperandContext): + return self.visitChildren(ctx) + + # Visit a parse tree produced by ExprParser#rightOperand. + def visitRightOperand(self, ctx: ExprParser.RightOperandContext): return self.visitChildren(ctx) # Visit a parse tree produced by ExprParser#rightMuldiv. def visitRightMuldiv(self, ctx: ExprParser.RightMuldivContext): return self.visitChildren(ctx) + # Visit a parse tree produced by ExprParser#rightPower. + def visitRightPower(self, ctx: ExprParser.RightPowerContext): + return self.visitChildren(ctx) + + # Visit a parse tree produced by ExprParser#rightPrimary. + def visitRightPrimary(self, ctx: ExprParser.RightPrimaryContext): + return self.visitChildren(ctx) + + # Visit a parse tree produced by ExprParser#rightExpression. + def visitRightExpression(self, ctx: ExprParser.RightExpressionContext): + return self.visitChildren(ctx) + # Visit a parse tree produced by ExprParser#rightAtom. def visitRightAtom(self, ctx: ExprParser.RightAtomContext): return self.visitChildren(ctx) diff --git a/src/gems_craft/expression/parsing/parse_expression.py b/src/gems_craft/expression/parsing/parse_expression.py index cddcad6d..f3193f6a 100644 --- a/src/gems_craft/expression/parsing/parse_expression.py +++ b/src/gems_craft/expression/parsing/parse_expression.py @@ -101,6 +101,12 @@ def visitAddsub(self, ctx: ExprParser.AddsubContext) -> ExpressionNode: return left - right raise ValueError(f"Invalid operator {op}") + # Visit a parse tree produced by ExprParser#power. + def visitPower(self, ctx: ExprParser.PowerContext) -> ExpressionNode: + base = ctx.expr(0).accept(self) # type: ignore + exponent = ctx.expr(1).accept(self) # type: ignore + return base**exponent + # Visit a parse tree produced by ExprParser#negation. def visitNegation(self, ctx: ExprParser.NegationContext) -> ExpressionNode: return -ctx.expr().accept(self) # type: ignore @@ -271,21 +277,16 @@ def visitShiftMuldiv(self, ctx: ExprParser.ShiftMuldivContext) -> ExpressionNode return left / right raise ValueError(f"Invalid operator {op}") - # Visit a parse tree produced by ExprParser#signedExpression. - def visitSignedExpression( - self, ctx: ExprParser.SignedExpressionContext + # Visit a parse tree produced by ExprParser#signedOperand. + def visitSignedOperand( + self, ctx: ExprParser.SignedOperandContext ) -> ExpressionNode: + # The sign applies to a whole shift operand, not just to an atom, so + # that "t - 2^2" means "t - (2^2)" and not "t + ((-2)^2)". + operand = ctx.shift_operand().accept(self) # type: ignore if ctx.op.text == "-": # type: ignore - return -ctx.expr().accept(self) # type: ignore - else: - return ctx.expr().accept(self) # type: ignore - - # Visit a parse tree produced by ExprParser#signedAtom. - def visitSignedAtom(self, ctx: ExprParser.SignedAtomContext) -> ExpressionNode: - if ctx.op.text == "-": # type: ignore - return -ctx.atom().accept(self) # type: ignore - else: - return ctx.atom().accept(self) # type: ignore + return -operand + return operand # Visit a parse tree produced by ExprParser#rightExpression. def visitRightExpression( @@ -308,6 +309,20 @@ def visitRightMuldiv(self, ctx: ExprParser.RightMuldivContext) -> ExpressionNode def visitRightAtom(self, ctx: ExprParser.RightAtomContext) -> ExpressionNode: return ctx.atom().accept(self) # type: ignore + # Visit a parse tree produced by ExprParser#rightOperand. + def visitRightOperand(self, ctx: ExprParser.RightOperandContext) -> ExpressionNode: + return ctx.shift_operand().accept(self) # type: ignore + + # Visit a parse tree produced by ExprParser#rightPower. + def visitRightPower(self, ctx: ExprParser.RightPowerContext) -> ExpressionNode: + base = ctx.shift_primary().accept(self) # type: ignore + exponent = ctx.shift_operand().accept(self) # type: ignore + return base**exponent + + # Visit a parse tree produced by ExprParser#rightPrimary. + def visitRightPrimary(self, ctx: ExprParser.RightPrimaryContext) -> ExpressionNode: + return ctx.shift_primary().accept(self) # type: ignore + _UNARY_FUNCTIONS = { "expec": ExpressionNode.expec, diff --git a/src/gems_craft/expression/print.py b/src/gems_craft/expression/print.py index 79ece5ce..ca5fdcd6 100644 --- a/src/gems_craft/expression/print.py +++ b/src/gems_craft/expression/print.py @@ -25,6 +25,7 @@ MinNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, TimeEvalNode, @@ -89,6 +90,11 @@ def division(self, node: DivisionNode) -> str: right_value = visit(node.right, self) return f"({left_value} / {right_value})" + def power(self, node: PowerNode) -> str: + left_value = visit(node.left, self) + right_value = visit(node.right, self) + return f"({left_value} ^ {right_value})" + def comparison(self, node: ComparisonNode) -> str: op = _COMPARISON_OPERATOR_TO_STRING[node.comparator] left_value = visit(node.left, self) diff --git a/src/gems_craft/expression/uses_sum_connections_on.py b/src/gems_craft/expression/uses_sum_connections_on.py index 0da90abe..16c4afd6 100644 --- a/src/gems_craft/expression/uses_sum_connections_on.py +++ b/src/gems_craft/expression/uses_sum_connections_on.py @@ -29,6 +29,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -64,6 +65,9 @@ def multiplication(self, node: MultiplicationNode) -> bool: def division(self, node: DivisionNode) -> bool: return visit(node.left, self) or visit(node.right, self) + def power(self, node: PowerNode) -> bool: + return visit(node.left, self) or visit(node.right, self) + def comparison(self, node: ComparisonNode) -> bool: return visit(node.left, self) or visit(node.right, self) diff --git a/src/gems_craft/expression/visitor.py b/src/gems_craft/expression/visitor.py index 3db8c6d7..04fe7deb 100644 --- a/src/gems_craft/expression/visitor.py +++ b/src/gems_craft/expression/visitor.py @@ -37,6 +37,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -74,6 +75,9 @@ def multiplication(self, node: MultiplicationNode) -> T: ... @abstractmethod def division(self, node: DivisionNode) -> T: ... + @abstractmethod + def power(self, node: PowerNode) -> T: ... + @abstractmethod def comparison(self, node: ComparisonNode) -> T: ... @@ -153,6 +157,8 @@ def visit(root: ExpressionNode, visitor: ExpressionVisitor[T]) -> T: return visitor.multiplication(root) elif isinstance(root, DivisionNode): return visitor.division(root) + elif isinstance(root, PowerNode): + return visitor.power(root) elif isinstance(root, ComparisonNode): return visitor.comparison(root) elif isinstance(root, TimeShiftNode): @@ -194,7 +200,7 @@ def visit(root: ExpressionNode, visitor: ExpressionVisitor[T]) -> T: class SupportsOperations(Protocol[T]): """ - Defines a type which implements math operations +, -, *, / + Defines a type which implements math operations +, -, *, /, ** """ @abstractmethod @@ -217,6 +223,10 @@ def __mul__(self, other: T) -> T: def __truediv__(self, other: T) -> T: pass + @abstractmethod + def __pow__(self, other: T) -> T: + pass + T_op = TypeVar("T_op", bound=SupportsOperations) @@ -224,7 +234,7 @@ def __truediv__(self, other: T) -> T: class ExpressionVisitorOperations(ExpressionVisitor[T_op], ABC): """ Provides default implementations of math operations - based on (+, -, /, *) operations of type T. + based on (+, -, /, *, **) operations of type T. """ def negation(self, node: NegationNode) -> T_op: @@ -246,3 +256,8 @@ def division(self, node: DivisionNode) -> T_op: left_value = visit(node.left, self) right_value = visit(node.right, self) return left_value / right_value + + def power(self, node: PowerNode) -> T_op: + left_value = visit(node.left, self) + right_value = visit(node.right, self) + return left_value**right_value diff --git a/src/gems_craft/model/port.py b/src/gems_craft/model/port.py index e46bf599..5671e09d 100644 --- a/src/gems_craft/model/port.py +++ b/src/gems_craft/model/port.py @@ -37,6 +37,7 @@ MinNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -151,6 +152,10 @@ def scenario_operator(self, node: ScenarioOperatorNode) -> None: def port_field(self, node: PortFieldNode) -> None: raise ValueError("Port definition cannot reference another port field.") + def power(self, node: PowerNode) -> None: + visit(node.left, self) + visit(node.right, self) + def floor(self, node: FloorNode) -> None: visit(node.operand, self) diff --git a/src/gems_craft/model/resolve_library.py b/src/gems_craft/model/resolve_library.py index be06f6a6..e9623fa1 100644 --- a/src/gems_craft/model/resolve_library.py +++ b/src/gems_craft/model/resolve_library.py @@ -31,6 +31,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -235,6 +236,10 @@ def division(self, node: DivisionNode) -> None: visit(node.left, self) visit(node.right, self) + def power(self, node: PowerNode) -> None: + visit(node.left, self) + visit(node.right, self) + def comparison(self, node: ComparisonNode) -> None: visit(node.left, self) visit(node.right, self) diff --git a/src/gems_runner/simulation/linearize.py b/src/gems_runner/simulation/linearize.py index 6b8f1ee5..d2a46a97 100644 --- a/src/gems_runner/simulation/linearize.py +++ b/src/gems_runner/simulation/linearize.py @@ -35,8 +35,10 @@ AdditionNode, CeilNode, FloorNode, + LiteralNode, MaxNode, MinNode, + PowerNode, RoundNode, VariableNode, ) @@ -112,6 +114,28 @@ def addition(self, node: AdditionNode) -> VectorizedExpr: # Overrides: nonlinear math functions (guard — variables not allowed) # # ------------------------------------------------------------------ # + def power(self, node: PowerNode) -> VectorizedExpr: + exponent = visit(node.right, self) + if not isinstance(exponent, xr.DataArray): + raise NotImplementedError( + "The exponent of '^' must be a parameter (DataArray) expression; " + "it cannot depend on decision variables in a linear programme." + ) + base = visit(node.left, self) + if isinstance(base, xr.DataArray): + return np.power(base, exponent) # type: ignore[return-value] + # A variable base only stays linear for the literal exponents 0 and 1, + # which the degree check lets through; anything else is nonlinear. + if isinstance(node.right, LiteralNode): + if node.right.value == 1: + return base + if node.right.value == 0: + return xr.DataArray(1.0) # type: ignore[return-value] + raise NotImplementedError( + "'^' is only supported for parameter (DataArray) expressions; " + "it cannot be used with decision variables in a linear programme." + ) + def floor(self, node: FloorNode) -> VectorizedExpr: operand = visit(node.operand, self) if isinstance(operand, xr.DataArray): diff --git a/src/gems_runner/simulation/vectorized_builder.py b/src/gems_runner/simulation/vectorized_builder.py index f6a831f6..cbd7d920 100644 --- a/src/gems_runner/simulation/vectorized_builder.py +++ b/src/gems_runner/simulation/vectorized_builder.py @@ -59,6 +59,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -366,6 +367,11 @@ def port_field_aggregator(self, node: PortFieldAggregatorNode) -> VectorizedExpr # Overridden in VectorizedLinearExprBuilder to raise when operands contain # linopy types: these operations cannot be expressed as linear constraints. + def power(self, node: PowerNode) -> VectorizedExpr: + base = visit(node.left, self) + exponent = visit(node.right, self) + return np.power(base, exponent) # type: ignore[return-value,arg-type,call-overload] + def floor(self, node: FloorNode) -> VectorizedExpr: operand = visit(node.operand, self) return np.floor(operand) # type: ignore[return-value,arg-type,call-overload] @@ -704,6 +710,9 @@ def division(self, node: DivisionNode) -> Optional[xr.DataArray]: def comparison(self, node: ComparisonNode) -> Optional[xr.DataArray]: return _and_mask(visit(node.left, self), visit(node.right, self)) + def power(self, node: PowerNode) -> Optional[xr.DataArray]: + return _and_mask(visit(node.left, self), visit(node.right, self)) + def floor(self, node: FloorNode) -> Optional[xr.DataArray]: return visit(node.operand, self) diff --git a/tests/e2e/models/operators/test_power_operator.py b/tests/e2e/models/operators/test_power_operator.py new file mode 100644 index 00000000..192d5656 --- /dev/null +++ b/tests/e2e/models/operators/test_power_operator.py @@ -0,0 +1,120 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +End-to-end coverage of the power operator '^' driven from YAML. + +Exercises '^' in a variable bound, a constraint, an objective contribution +(parameters and literals only) and in an extra-output (applied to a decision +variable, which is only legal post-solve). +""" + +import io +import math +from pathlib import Path + +import pytest + +from gems_craft.model.parsing import parse_yaml_library +from gems_craft.model.resolve_library import resolve_library +from gems_craft.study import Study +from gems_craft.study.parsing import parse_yaml_system +from gems_craft.study.resolve_components import build_data_base, resolve_system +from gems_runner.simulation import build_problem +from gems_runner.simulation.simulation_table import SimulationTableBuilder +from gems_runner.simulation.time_block import TimeBlock + +LIBRARY_YAML = """ +library: + id: power_lib + description: Exercises the '^' operator in every expression context. + port-types: [] + models: + - id: powered + parameters: + - id: p + - id: cost + variables: + - id: gen + lower-bound: 0 + upper-bound: p^3 + variable-type: continuous + constraints: + - id: fix_generation + expression: gen = p^2 + objective-contributions: + - id: objective + expression: expec(sum(cost^2 * gen)) + extra-outputs: + - id: squared + expression: gen^2 + - id: two_to_the_p + expression: 2^p + - id: precedence + expression: gen + -2^2 +""" + +SYSTEM_YAML = """ +system: + components: + - id: unit + model: power_lib.powered + parameters: + - id: p + scenario-dependent: false + time-dependent: false + value: 3.0 + - id: cost + scenario-dependent: false + time-dependent: false + value: 2.0 +""" + +# p = 3, cost = 2, over 2 timesteps: +# gen = p^2 = 9 (upper bound p^3 = 27 is not binding) +# obj = cost^2 * gen * 2 timesteps = 4 * 9 * 2 = 72 +EXPECTED_GENERATION = 9.0 +EXPECTED_OBJECTIVE = 72.0 + + +@pytest.fixture +def study(tmp_path: Path) -> Study: + library = resolve_library([parse_yaml_library(io.StringIO(LIBRARY_YAML))]) + system = resolve_system(parse_yaml_system(io.StringIO(SYSTEM_YAML)), library) + database = build_data_base(parse_yaml_system(io.StringIO(SYSTEM_YAML)), tmp_path) + return Study(system, database) + + +def test_power_operator_end_to_end(study: Study) -> None: + problem = build_problem(study, TimeBlock(1, [0, 1]), list(range(1))) + problem.solve(solver_name="highs") + + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, EXPECTED_OBJECTIVE, rel_tol=1e-6) + + df = SimulationTableBuilder().build(problem) + + def value(output: str, time_index: int = 0) -> float: + return ( + df.component("unit") + .output(output) + .value(time_index=time_index, scenario_index=0) + ) + + assert value("gen") == pytest.approx(EXPECTED_GENERATION) + # '^' applied to a decision variable: legal in an extra-output only. + assert value("squared") == pytest.approx(81.0) + # '^' with a parameter exponent. + assert value("two_to_the_p") == pytest.approx(8.0) + # '^' binds tighter than unary minus: -2^2 is -(2^2) = -4, not (-2)^2 = 4, + # so this is 9 - 4 and not 9 + 4. + assert value("precedence") == pytest.approx(5.0) diff --git a/tests/unittests/gems_craft/expressions/parsing/test_expression_parsing.py b/tests/unittests/gems_craft/expressions/parsing/test_expression_parsing.py index a8c253ec..dfa046ef 100644 --- a/tests/unittests/gems_craft/expressions/parsing/test_expression_parsing.py +++ b/tests/unittests/gems_craft/expressions/parsing/test_expression_parsing.py @@ -293,6 +293,86 @@ def test_parse_upper_bound_unknown_variable_raises() -> None: parse_expression("upper_bound(p)", identifiers) +@pytest.mark.parametrize( + "variables, parameters, expression_str, expected", + [ + ({}, {"p"}, "p^2", param("p") ** 2), + ({}, {"p"}, "2^p", literal(2) ** param("p")), + ({}, {"p", "q"}, "p^(1+q)", param("p") ** (literal(1) + param("q"))), + ({"x"}, {}, "x[t-1]^2", var("x").shift(-literal(1)) ** 2), + ({"x"}, {}, "x^2", var("x") ** 2), + ], +) +def test_parsing_power( + variables: Set[str], + parameters: Set[str], + expression_str: str, + expected: ExpressionNode, +) -> None: + identifiers = ModelIdentifiers(variables, parameters) + assert expressions_equal(parse_expression(expression_str, identifiers), expected) + + +@pytest.mark.parametrize( + "expression_str, equivalent_str", + [ + # '^' binds tighter than unary minus: standard mathematical convention, + # a deliberate deviation from Antares Simulator, where '-2^2' is 4. + ("-2^2", "-(2^2)"), + ("2^-3", "2^(-3)"), + # right-associative + ("2^3^2", "2^(3^2)"), + # '^' binds tighter than '*' and '/' + ("2*3^2", "2*(3^2)"), + ("2^3*2", "(2^3)*2"), + ("2^3/2", "(2^3)/2"), + # ... and than '+' and '-' + ("1+2^3", "1+(2^3)"), + # inside a time shift, the sign applies to the whole power operand, + # so the shift amount is -(2^2) = -4 and not (-2)^2 = 4 + ("x[t-2^2]", "x[t-(2^2)]"), + # and '^' does not swallow a trailing '*' on its right: + # the shift amount is (-(2^2))*3 = -12 and not -(2^(2*3)) = -64 + ("x[t-2^2*3]", "x[t-(2^2)*3]"), + ("x[t-2*3^2]", "x[t-2*(3^2)]"), + ], +) +def test_power_precedence(expression_str: str, equivalent_str: str) -> None: + """Each expression must parse exactly like its parenthesised equivalent.""" + identifiers = ModelIdentifiers(variables={"x"}, parameters=set()) + assert expressions_equal( + parse_expression(expression_str, identifiers), + parse_expression(equivalent_str, identifiers), + ) + + +@pytest.mark.parametrize( + "expression_str", + [ + "p^2", + "2^p", + "-2^2", + "2^-3", + "2^3^2", + "2*3^2", + "2^3*2", + "p^(1+q)", + "(p^2)*(q^3)", + "-(p^2) + q^2", + ], +) +def test_power_print_reparse_round_trip(expression_str: str) -> None: + """Printing an AST containing '^' and re-parsing it must be a no-op. + + Time-shifted expressions are out of scope here: PrinterVisitor emits them + as ``x.shift(...)``, which the grammar does not accept — a pre-existing + limitation unrelated to '^'. + """ + identifiers = ModelIdentifiers(variables={"x"}, parameters={"p", "q"}) + expr = parse_expression(expression_str, identifiers) + assert expressions_equal(parse_expression(print_expr(expr), identifiers), expr) + + @pytest.mark.parametrize( "expression_str", [ @@ -301,6 +381,9 @@ def test_parse_upper_bound_unknown_variable_raises() -> None: "x[t+1-t]", "x[2*t]", "x[t 4]", + # A signed exponent is not allowed inside a time shift: a fractional + # shift is meaningless. '2^-1' remains valid outside a shift. + "x[t+2^-1]", ], ) def test_parse_cancellation_should_throw(expression_str: str) -> None: diff --git a/tests/unittests/gems_craft/expressions/visitor/test_copy.py b/tests/unittests/gems_craft/expressions/visitor/test_copy.py index 3d776ec1..8e6238f8 100644 --- a/tests/unittests/gems_craft/expressions/visitor/test_copy.py +++ b/tests/unittests/gems_craft/expressions/visitor/test_copy.py @@ -25,6 +25,7 @@ from gems_craft.expression.expression import ( AllTimeSumNode, MultiplicationNode, + PowerNode, TimeEvalNode, TimeShiftNode, ) @@ -44,3 +45,16 @@ def test_copy_ast() -> None: ) copy = copy_expression(ast) assert expressions_equal(ast, copy) + + +def test_copy_power_ast() -> None: + ast = MultiplicationNode( + PowerNode(VariableNode("x"), LiteralNode(2)), + PowerNode( + ParameterNode("p"), AdditionNode([LiteralNode(1), ParameterNode("q")]) + ), + ) + copy = copy_expression(ast) + assert expressions_equal(ast, copy) + assert isinstance(copy, MultiplicationNode) + assert isinstance(copy.left, PowerNode) diff --git a/tests/unittests/gems_craft/expressions/visitor/test_degree.py b/tests/unittests/gems_craft/expressions/visitor/test_degree.py index 0ceb958c..6c5efe6c 100644 --- a/tests/unittests/gems_craft/expressions/visitor/test_degree.py +++ b/tests/unittests/gems_craft/expressions/visitor/test_degree.py @@ -23,6 +23,7 @@ var, visit, ) +from gems_craft.expression.degree import is_constant, is_linear from gems_craft.expression.expression import ( AbsNode, CeilNode, @@ -102,3 +103,35 @@ def test_degree_computation_should_take_into_account_simplifications() -> None: expr = LiteralNode(0) * x assert visit(expr, ExpressionDegreeVisitor()) == 0 + + +def test_power_degree() -> None: + x = var("x") + p = param("p") + + # Literals and parameters stay constant, whatever the exponent. + assert visit(param("p") ** 2, ExpressionDegreeVisitor()) == 0 + assert visit(LiteralNode(2) ** p, ExpressionDegreeVisitor()) == 0 + assert visit(p ** (1 + param("q")), ExpressionDegreeVisitor()) == 0 + + # A variable base raised to a non-negative integer literal stays polynomial. + assert visit(x**2, ExpressionDegreeVisitor()) == 2 + assert visit(x**1, ExpressionDegreeVisitor()) == 1 + assert visit(x**0, ExpressionDegreeVisitor()) == 0 + assert visit((x * x) ** 2, ExpressionDegreeVisitor()) == 4 + + # Any other exponent on a variable base is not polynomial. + assert visit(x**p, ExpressionDegreeVisitor()) == math.inf + assert visit(x**0.5, ExpressionDegreeVisitor()) == math.inf + assert visit(x ** (-2), ExpressionDegreeVisitor()) == math.inf + + # An inf-degree base stays inf, including for the exponent 0 (inf * 0 is nan). + assert visit(FloorNode(x) ** 2, ExpressionDegreeVisitor()) == math.inf + assert visit(FloorNode(x) ** 0, ExpressionDegreeVisitor()) == 0 + + +def test_variable_exponent_degree() -> None: + """A variable exponent is not polynomial, but must not break is_linear().""" + assert visit(param("p") ** var("x"), ExpressionDegreeVisitor()) == math.inf + assert not is_linear(param("p") ** var("x")) + assert not is_constant(param("p") ** var("x")) diff --git a/tests/unittests/gems_craft/expressions/visitor/test_equality.py b/tests/unittests/gems_craft/expressions/visitor/test_equality.py index 9fdae1d4..95520b34 100644 --- a/tests/unittests/gems_craft/expressions/visitor/test_equality.py +++ b/tests/unittests/gems_craft/expressions/visitor/test_equality.py @@ -44,6 +44,8 @@ var("x").round(), maximum(var("x"), param("p")), minimum(var("x"), param("p")), + var("x") ** 2, + param("p") ** (param("q") + 1), ], ) def test_equals(expr: ExpressionNode) -> None: @@ -80,6 +82,10 @@ def test_equals(expr: ExpressionNode) -> None: (maximum(var("x"), param("p")), maximum(var("y"), param("p"))), (minimum(var("x"), param("p")), minimum(var("x"), param("q"))), (maximum(var("x"), param("p")), minimum(var("x"), param("p"))), # Max vs Min + # power + (var("x") ** 2, var("y") ** 2), + (var("x") ** 2, var("x") ** 3), + (var("x") ** 2, var("x") * var("x")), # different node type ], ) def test_not_equals(lhs: ExpressionNode, rhs: ExpressionNode) -> None: diff --git a/tests/unittests/gems_craft/expressions/visitor/test_printer.py b/tests/unittests/gems_craft/expressions/visitor/test_printer.py index 37d406e7..c6bb0d1d 100644 --- a/tests/unittests/gems_craft/expressions/visitor/test_printer.py +++ b/tests/unittests/gems_craft/expressions/visitor/test_printer.py @@ -62,6 +62,16 @@ def test_dual_reduced_cost_printer() -> None: assert visit(ReducedCostNode("p"), PrinterVisitor()) == "reduced_cost(p)" +def test_power_printer() -> None: + p = param("p") + q = param("q") + + assert visit(p**2, PrinterVisitor()) == "(p ^ 2.0)" + assert visit(p ** (q + 1), PrinterVisitor()) == "(p ^ (q + 1.0))" + assert visit(-(p**2), PrinterVisitor()) == "-((p ^ 2.0))" + assert visit(p**q**2, PrinterVisitor()) == "(p ^ (q ^ 2.0))" + + def test_lower_upper_bound_printer() -> None: assert visit(LowerBoundNode("x"), PrinterVisitor()) == "lower_bound(x)" assert visit(UpperBoundNode("x"), PrinterVisitor()) == "upper_bound(x)" diff --git a/tests/unittests/gems_craft/lib_parsing/test_lib_parsing.py b/tests/unittests/gems_craft/lib_parsing/test_lib_parsing.py index a2c9e08f..f22d9207 100644 --- a/tests/unittests/gems_craft/lib_parsing/test_lib_parsing.py +++ b/tests/unittests/gems_craft/lib_parsing/test_lib_parsing.py @@ -275,6 +275,70 @@ def test_dual_in_constraint_is_rejected() -> None: resolve_library([input_lib]) +def test_power_of_variable_in_constraint_is_rejected() -> None: + lib_yaml = io.StringIO(""" +library: + id: basic + port-types: [] + models: + - id: bad-model + variables: + - id: x + variable-type: continuous + parameters: [] + ports: [] + constraints: + - id: bad + expression: x^2 = 0 +""") + input_lib = parse_yaml_library(lib_yaml) + with pytest.raises(ValueError, match="Non-linear expression is not allowed"): + resolve_library([input_lib]) + + +def test_power_of_parameter_in_constraint_is_accepted() -> None: + lib_yaml = io.StringIO(""" +library: + id: basic + port-types: [] + models: + - id: good-model + variables: + - id: x + variable-type: continuous + parameters: + - id: p + ports: [] + constraints: + - id: good + expression: p^2 * x <= 2^p +""") + input_lib = parse_yaml_library(lib_yaml) + resolve_library([input_lib]) + + +def test_variable_exponent_in_constraint_is_rejected() -> None: + lib_yaml = io.StringIO(""" +library: + id: basic + port-types: [] + models: + - id: bad-model + variables: + - id: x + variable-type: continuous + parameters: + - id: p + ports: [] + constraints: + - id: bad + expression: p^x = 0 +""") + input_lib = parse_yaml_library(lib_yaml) + with pytest.raises(ValueError, match="Non-linear expression is not allowed"): + resolve_library([input_lib]) + + def test_reduced_cost_in_objective_is_rejected() -> None: lib_yaml = io.StringIO(""" library: diff --git a/tests/unittests/gems_runner/expression/test_evaluation.py b/tests/unittests/gems_runner/expression/test_evaluation.py index 785ecb9b..74681fd2 100644 --- a/tests/unittests/gems_runner/expression/test_evaluation.py +++ b/tests/unittests/gems_runner/expression/test_evaluation.py @@ -33,6 +33,10 @@ ReducedCostNode, UpperBoundNode, ) +from gems_craft.expression.parsing.parse_expression import ( + ModelIdentifiers, + parse_expression, +) from gems_runner.expression import EvaluationContext, EvaluationVisitor, ValueProvider @@ -117,6 +121,37 @@ def test_dual_reduced_cost_evaluation_raises() -> None: visit(ReducedCostNode("p"), EvaluationVisitor(ctx)) +def test_power_evaluation() -> None: + x = var("x") + p = param("p") + context = EvaluationContext(variables={"x": 3}, parameters={"p": 2}) + + assert visit(x**2, EvaluationVisitor(context)) == pytest.approx(9.0) + assert visit(x ** (2 + p), EvaluationVisitor(context)) == pytest.approx(81.0) + assert visit(x ** (-p), EvaluationVisitor(context)) == pytest.approx(1 / 9) + assert visit(x**0.5, EvaluationVisitor(context)) == pytest.approx(3**0.5) + assert visit(literal(2) ** p, EvaluationVisitor(context)) == pytest.approx(4.0) + + +def test_power_precedence_evaluation() -> None: + """The parsed form must evaluate the way standard notation reads.""" + identifiers = ModelIdentifiers(variables=set(), parameters=set()) + context = EvaluationContext(variables={}, parameters={}) + + def value(expression_str: str) -> float: + expr = parse_expression(expression_str, identifiers) + return visit(expr, EvaluationVisitor(context)) + + assert value("-2^2") == pytest.approx(-4.0) # not (-2)^2 = 4 + assert value("2^-3") == pytest.approx(0.125) + assert value("2^3^2") == pytest.approx(512.0) # not (2^3)^2 = 64 + assert value("2*3^2") == pytest.approx(18.0) + assert value("2^3*2") == pytest.approx(16.0) + # the shift amounts of "x[t-2^2]" and "x[t-2^2*3]" + assert value("-2^2") == pytest.approx(-4.0) + assert value("-2^2*3") == pytest.approx(-12.0) + + def test_lower_upper_bound_evaluation_raises() -> None: ctx = EvaluationContext() with pytest.raises(NotImplementedError, match="lower_bound"): diff --git a/tests/unittests/gems_runner/simulation/test_simulation_table_extra_outputs.py b/tests/unittests/gems_runner/simulation/test_simulation_table_extra_outputs.py index 6f81ce12..5a3ec7cd 100644 --- a/tests/unittests/gems_runner/simulation/test_simulation_table_extra_outputs.py +++ b/tests/unittests/gems_runner/simulation/test_simulation_table_extra_outputs.py @@ -188,6 +188,59 @@ def test_extra_output_abs_round_on_variable() -> None: assert rounded == pytest.approx(3.0) +def test_extra_output_power_on_variable() -> None: + """ + myVar^(2 + myParam) is allowed in extra outputs (post-solve evaluation), + even though a variable base is rejected as nonlinear inside a constraint. + Covers negative and fractional exponents too. + """ + from gems_craft.expression import param, var + from gems_craft.expression.expression import literal + from gems_craft.model.model import model + from gems_craft.model.parameter import float_parameter + from gems_craft.model.variable import float_variable + from gems_craft.study import ConstantData, DataBase, Study, System, create_component + from gems_runner.simulation import TimeBlock, build_problem + + SIMPLE_MODEL = model( + id="SIMPLE_POWER", + parameters=[float_parameter("p")], + variables=[float_variable("a", lower_bound=literal(4), upper_bound=literal(4))], + extra_outputs={ + "powered": var("a") ** (literal(2) + param("p")), + "inverse": var("a") ** literal(-1), + "root": var("a") ** literal(0.5), + }, + # linopy >= 0.9 refuses to solve a model with no objective. + objective_contributions={ + "null_objective": (literal(0) * var("a")).time_sum().expec() + }, + ) + + database = DataBase() + comp = create_component(model=SIMPLE_MODEL, id="comp_1") + database.add_data("comp_1", "p", ConstantData(1)) + + system = System("test_power_extra") + system.add_component(comp) + + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), scenario_ids=list(range(1)) + ) + problem.solve(solver_name="highs") + + df = SimulationTableBuilder().build(problem) + + def value(output: str) -> float: + return ( + df.component("comp_1").output(output).value(time_index=0, scenario_index=0) + ) + + assert value("powered") == pytest.approx(64.0) # 4^(2+1) + assert value("inverse") == pytest.approx(0.25) + assert value("root") == pytest.approx(2.0) + + def test_extra_output_min_on_variable() -> None: """ min(variable, parameter) is allowed in extra outputs and evaluated diff --git a/tests/unittests/gems_runner/simulation/test_vectorized_linear_expr_builder.py b/tests/unittests/gems_runner/simulation/test_vectorized_linear_expr_builder.py index 3d1a74a6..3a29163d 100644 --- a/tests/unittests/gems_runner/simulation/test_vectorized_linear_expr_builder.py +++ b/tests/unittests/gems_runner/simulation/test_vectorized_linear_expr_builder.py @@ -361,6 +361,78 @@ def test_comparison_equal_also_raises(builder: VectorizedLinearExprBuilder) -> N visit(literal(1) == literal(1), builder) +# --------------------------------------------------------------------------- +# 8b. power() — linopy guard +# --------------------------------------------------------------------------- + + +def test_power_on_literals(builder: VectorizedLinearExprBuilder) -> None: + result = visit(literal(2.0) ** literal(3.0), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(8.0) + + +def test_power_on_parameter_matches_manual_expansion( + builder: VectorizedLinearExprBuilder, param_da: xr.DataArray +) -> None: + """p^3 must give exactly the coefficients of p*p*p.""" + powered = visit(param("p") ** literal(3.0), builder) + expanded = visit(param("p") * param("p") * param("p"), builder) + assert isinstance(powered, xr.DataArray) + xr.testing.assert_allclose(powered, expanded) + np.testing.assert_allclose(powered.values, param_da.values**3) + + +def test_power_with_parameter_exponent( + builder: VectorizedLinearExprBuilder, param_da: xr.DataArray +) -> None: + result = visit(literal(2.0) ** param("p"), builder) + np.testing.assert_allclose(result.values, 2.0**param_da.values) + + +def test_power_with_negative_and_fractional_exponent( + builder: VectorizedLinearExprBuilder, param_da: xr.DataArray +) -> None: + negative = visit(param("p") ** literal(-1.0), builder) + np.testing.assert_allclose(negative.values, 1.0 / param_da.values) + fractional = visit(param("p") ** literal(0.5), builder) + np.testing.assert_allclose(fractional.values, param_da.values**0.5) + + +def test_power_as_linear_coefficient(builder: VectorizedLinearExprBuilder) -> None: + """A parameter power used as a coefficient builds a linear expression.""" + result = visit((param("p") ** literal(2.0)) * var("x"), builder) + assert isinstance(result, linopy.LinearExpression) + + +def test_power_on_variable_base_raises(builder: VectorizedLinearExprBuilder) -> None: + with pytest.raises(NotImplementedError, match=r"\^"): + visit(var("x") ** literal(2.0), builder) + + +def test_power_with_variable_exponent_raises( + builder: VectorizedLinearExprBuilder, +) -> None: + with pytest.raises(NotImplementedError, match="exponent"): + visit(literal(2.0) ** var("x"), builder) + + +def test_power_of_variable_to_one_is_identity( + builder: VectorizedLinearExprBuilder, +) -> None: + """Exponents 0 and 1 pass the degree check, so the builder must handle them.""" + result = visit(var("x") ** literal(1.0), builder) + assert isinstance(result, linopy.Variable) + + +def test_power_of_variable_to_zero_is_one( + builder: VectorizedLinearExprBuilder, +) -> None: + result = visit(var("x") ** literal(0.0), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(1.0) + + # --------------------------------------------------------------------------- # 9. floor() / ceil() / abs() / round() — linopy guard # ---------------------------------------------------------------------------