From aac2abbe78f8b566748916e8484f1862e9ec2cfe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 12:44:37 +0000 Subject: [PATCH 1/7] feat(expression): support the power operator `^` Adds the binary power operator `^` to the GEMS expression language, as in Antares Simulator. It was previously not even a lexer token, so a library YAML valid for the C++ interpreter failed to load in GemsPy. Grammar (`grammar/Expr.g4`): - `^` is right-associative and binds tighter than unary minus and than `*` `/`, following standard mathematical notation. This is a deliberate deviation from Antares Simulator, where `-2^2` currently parses as `(-2)^2 = 4`; here it parses as `-(2^2) = -4`. - Inside a time shift, the sign's operand is widened to a power operand so that `x[t-2^2]` shifts by -4, and the right-hand side of `^` uses a dedicated `pow_expr` rule rather than `right_expr`, so `^` does not swallow a trailing `*` (`x[t-2^2*3]` shifts by -12). `signedAtom` and `signedExpression` merge into a single `signedOperand` alternative. - A signed exponent is rejected inside a time shift: `x[t+2^-1]` is a parse error, a fractional shift being meaningless. - `**` is not an alias: GEMS accepts `^` only, matching Antares. AST and visitors: - New `PowerNode(BinaryOperatorNode)` plus `__pow__` / `__rpow__` on `ExpressionNode`, and `__pow__` on the `SupportsOperations` protocol so `ExpressionVisitorOperations` supplies a default `power`. - Explicit `power` implementations for the visitors that do not derive from it. Degree and linearity: - The exponent must not depend on a variable, in any context. A degree-0 base stays degree 0; otherwise the degree is `base_degree * exponent` for a non-negative integer literal exponent, and infinite otherwise. So `x^2` in a constraint raises the existing non-linearity error, while `extra-outputs` accept it. The linear builder handles the literal exponents 0 and 1, which the degree check lets through. Also fixes the stale output path in `grammar/generate-parser.sh` (it still pointed at the pre-rename `src/gems/...`) and in `grammar/README.md`. Documentation: new "Expression syntax" user-guide page covering the operator set, precedence, the `-2^2` convention and the Antares divergence. No change to solver results for existing studies: `^` was previously a parse error, so no GEMS file in circulation can contain it, and the precedence of the existing operators is unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015DqK4sz5N7o9FW3uyRjq6U --- docs/CHANGELOG.md | 17 + docs/user-guide/expressions.md | 123 ++ grammar/Expr.g4 | 22 +- grammar/README.md | 4 +- grammar/generate-parser.sh | 2 +- mkdocs.yml | 1 + src/gems_craft/expression/__init__.py | 1 + src/gems_craft/expression/degree.py | 24 + src/gems_craft/expression/equality.py | 6 + src/gems_craft/expression/expression.py | 17 + src/gems_craft/expression/indexing.py | 4 + .../expression/parsing/antlr/Expr.interp | 6 +- .../expression/parsing/antlr/Expr.tokens | 38 +- .../expression/parsing/antlr/ExprLexer.interp | 5 +- .../expression/parsing/antlr/ExprLexer.py | 526 +++--- .../expression/parsing/antlr/ExprLexer.tokens | 38 +- .../expression/parsing/antlr/ExprParser.py | 1660 ++++++++++------- .../expression/parsing/antlr/ExprVisitor.py | 33 +- .../expression/parsing/parse_expression.py | 41 +- src/gems_craft/expression/print.py | 6 + .../expression/uses_sum_connections_on.py | 4 + src/gems_craft/expression/visitor.py | 19 +- src/gems_craft/model/port.py | 5 + src/gems_craft/model/resolve_library.py | 5 + src/gems_runner/simulation/linearize.py | 24 + .../simulation/vectorized_builder.py | 9 + .../models/operators/test_power_operator.py | 120 ++ .../parsing/test_expression_parsing.py | 83 + .../expressions/visitor/test_copy.py | 14 + .../expressions/visitor/test_degree.py | 32 + .../expressions/visitor/test_equality.py | 6 + .../expressions/visitor/test_printer.py | 10 + .../lib_parsing/test_lib_parsing.py | 66 + .../gems_runner/expression/test_evaluation.py | 35 + .../test_simulation_table_extra_outputs.py | 53 + .../test_vectorized_linear_expr_builder.py | 72 + 36 files changed, 2111 insertions(+), 1020 deletions(-) create mode 100644 docs/user-guide/expressions.md create mode 100644 tests/e2e/models/operators/test_power_operator.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 33e98a9c..6f448d6b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,6 +9,23 @@ All notable changes to GemsPy are documented here. to **3.11** accordingly (linopy 0.9 requires Python >= 3.11). ### Added +- **Power operator `^` in the expression language** - `^` is now accepted in + constraints, objective contributions, variable bounds, port-field definitions + and extra-outputs, matching Antares Simulator. It is right-associative and + binds tighter than unary minus and than `*` `/`. Its operands must be + literals or parameters everywhere except in `extra-outputs`, where a decision + variable may be raised to a power (evaluated post-solve), like the existing + `floor` / `ceil` / `abs` / `round` / `min` / `max`. The exponent must never + depend on a decision variable. The Python API gains `**` on `ExpressionNode`. + See [Expression syntax](user-guide/expressions.md). + + **Divergence from Antares Simulator:** GemsPy parses `-2^2` as `-(2^2) = -4`, + the standard mathematical convention, whereas Antares Simulator currently + parses it as `(-2)^2 = 4`. The same model can therefore give different + results in the two interpreters until the Antares side is aligned. No + existing study is affected: `^` was previously a parse error, so no GEMS file + in circulation can contain it. + - **Integer strategy and thermal heuristics** - components can now set `integer-strategy` (`exact` (default), `relaxed`, or `heuristic` + `heuristic-id`) to control how their model's integer/binary variables are diff --git a/docs/user-guide/expressions.md b/docs/user-guide/expressions.md new file mode 100644 index 00000000..353d41e6 --- /dev/null +++ b/docs/user-guide/expressions.md @@ -0,0 +1,123 @@ +# Expression syntax + +Constraints, objective contributions, variable bounds, port-field definitions +and extra-outputs are all written in the GEMS expression language. This page +documents the syntax **as the GemsPy interpreter accepts it**. For the language +itself, see the +[GEMS documentation](https://gems-energy.readthedocs.io/en/latest/user-guide/introduction/). + +--- + +## Operators + +| Operator | Meaning | Notes | +|---|---|---| +| `+` `-` | addition, subtraction | `-x` is also valid as unary minus | +| `*` `/` | multiplication, division | | +| `^` | power | right-associative, binds tighter than unary minus | +| `=` `<=` `>=` | comparison | only at the top level of a constraint | + +## Functions + +| Function | Meaning | +|---|---| +| `floor(e)` / `ceil(e)` | round down / up to the nearest integer | +| `round(e)` | round to the nearest integer (banker's rounding) | +| `abs(e)` | absolute value | +| `min(a, b, ...)` / `max(a, b, ...)` | element-wise minimum / maximum | +| `expec(e)` | expectation over scenarios | +| `sum(e)` | sum over all timesteps | +| `sum(t-a..t+b, e)` | sum over a time window | +| `sum_connections(port.field)` | sum of a port field over all connections | +| `dual(constraint_id)` | dual value of a constraint (extra-outputs only) | +| `reduced_cost(variable_id)` | reduced cost of a variable (extra-outputs only) | + +## Time indexing + +| Syntax | Meaning | +|---|---| +| `x[t]` | value at the current timestep (equivalent to `x`) | +| `x[t-1]`, `x[t+d]` | value at a timestep relative to the current one | +| `x[3]` | value at an absolute timestep | +| `(expr)[t-1]` | shift applied to a whole expression | + +--- + +## Precedence + +From tightest to loosest binding: + +1. `^` +2. unary `-` +3. `*` `/` +4. `+` `-` +5. `=` `<=` `>=` + +`^` is **right-associative**; all the other binary operators are +left-associative. Parentheses override precedence anywhere. + +### The power operator and unary minus + +`^` binds **tighter than unary minus**, following standard mathematical +notation: + +| Expression | Parses as | Value | +|---|---|---| +| `-2^2` | `-(2^2)` | `-4` | +| `2^-3` | `2^(-3)` | `0.125` | +| `2^3^2` | `2^(3^2)` | `512` | +| `2*3^2` | `2*(3^2)` | `18` | +| `2^3*2` | `(2^3)*2` | `16` | + +!!! warning "Divergence from Antares Simulator" + Antares Simulator currently parses `-2^2` as `(-2)^2 = 4`. GemsPy follows + the standard convention and evaluates it to `-4`. Write `(-2)^2` + explicitly if you need the same result in both interpreters. + +### Inside time-shift brackets + +Within `x[t ...]`, the sign applies to the whole power operand and `^` does not +swallow a trailing `*` or `/`: + +| Expression | Shift amount | +|---|---| +| `x[t-2^2]` | `-4` | +| `x[t-2^2*3]` | `-12` | +| `x[t-2*3^2]` | `-18` | + +A **signed exponent is not allowed** inside a time shift: `x[t+2^-1]` is a +parse error, since a fractional shift is meaningless. `2^-1` remains valid +everywhere else. + +--- + +## Where nonlinear expressions are allowed + +`^`, `floor`, `ceil`, `abs`, `round`, `min` and `max` may be applied to +**literals and parameters** anywhere. Applying them to a **decision variable** +produces a nonlinear expression, which is: + +- **rejected** in constraints, objective contributions and variable bounds, + with `Non-linear expression is not allowed in ...`; +- **allowed** in `extra-outputs`, which are evaluated after the solve. + +So `p^2`, `2^p` and `p^(1+q)` are valid in a constraint, `x^2` is not, and +`x^(2 + p)` is valid in an extra-output. + +The **exponent must never depend on a decision variable**, in any context: +`p^x` raises `Exponent of a power expression must not depend on variables.` + +~~~ yaml +constraints: + - id: capacity + expression: gen <= p_max^2 # parameters only — fine + +extra-outputs: + - id: squared_generation + expression: gen^2 # variable base — post-solve only +~~~ + +!!! note + `**` is not an alias for `^`. GEMS accepts `^` only, matching Antares + Simulator. (The GemsPy *Python* API does support `param("p") ** 2`, since + that is Python's own operator.) diff --git a/grammar/Expr.g4 b/grammar/Expr.g4 index f990402f..017ad361 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 @@ -60,13 +61,28 @@ 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=('+' | '-') pow_expr # signedOperand ; right_expr : right_expr op=('/' | '*') right_expr # rightMuldiv - | '(' expr ')' # rightExpression + | pow_expr # rightPow + ; + +// Operand of '^' inside a time shift, chained right-associatively. +// Deliberately NOT "right_expr" on the right of '^': being a separate rule, +// it would be entered at precedence 0 and greedily swallow '*' and '/', +// so "t - 2^2*3" would mean "2^(2*3)" instead of "(2^2)*3". +// The exponent is unsigned here, so "t + 2^-1" is a parse error: a fractional +// time shift is meaningless, and negative exponents remain available in the +// general "expr" rule. +pow_expr + : pow_atom '^' pow_expr # rightPower + | pow_atom # rightPowAtom + ; + +pow_atom + : '(' 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/mkdocs.yml b/mkdocs.yml index 24e49165..bd1aa5e4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,7 @@ nav: - 'User guide: GEMS language and data structure': 'user-guide.md' - 'User guide: GemsPy interpreter package': - 'Reading input files': 'user-guide/inputs.md' + - 'Expression syntax': 'user-guide/expressions.md' - 'Building systems with the Python API': 'user-guide/building.md' - 'Optimization: building and solving problems': 'user-guide/optimisation.md' - 'Retrieving outputs': 'user-guide/outputs.md' 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 97c007fc..14e3ea60 100644 --- a/src/gems_craft/expression/degree.py +++ b/src/gems_craft/expression/degree.py @@ -39,12 +39,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. @@ -70,6 +75,25 @@ 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: + # Mirrors the Antares Simulator rule: the exponent must not depend on + # variables, and a variable base only stays polynomial for a + # non-negative integer exponent. + if visit(node.right, self) != 0: + raise ValueError( + "Exponent of a power expression must not depend on variables." + ) + 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 1e5b356d..2774cf41 100644 --- a/src/gems_craft/expression/equality.py +++ b/src/gems_craft/expression/equality.py @@ -36,6 +36,7 @@ MinNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -75,6 +76,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): @@ -145,6 +148,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 00fdc9eb..7934bf57 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 a74d6a7c..eda7feb2 100644 --- a/src/gems_craft/expression/indexing.py +++ b/src/gems_craft/expression/indexing.py @@ -34,6 +34,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -94,6 +95,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..b0092611 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 +pow_expr +pow_atom 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..2bfdd10b 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_pow_expr = 8 + RULE_pow_atom = 9 ruleNames = [ "portFieldExpr", @@ -1434,6 +1515,8 @@ class ExprParser(Parser): "shift", "shift_expr", "right_expr", + "pow_expr", + "pow_atom", ] 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 pow_expr(self): + return self.getTypedRuleContext(ExprParser.Pow_exprContext, 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.pow_expr() 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 RightPowContext(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 pow_expr(self): + return self.getTypedRuleContext(ExprParser.Pow_exprContext, 0) def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitRightExpression"): - return visitor.visitRightExpression(self) + if hasattr(visitor, "visitRightPow"): + return visitor.visitRightPow(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.RightPowContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 136 + self.pow_expr() 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 Pow_exprContext(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_pow_expr + + def copyFrom(self, ctx: ParserRuleContext): + super().copyFrom(ctx) + + class RightPowerContext(Pow_exprContext): + + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.Pow_exprContext + super().__init__(parser) + self.copyFrom(ctx) + + def pow_atom(self): + return self.getTypedRuleContext(ExprParser.Pow_atomContext, 0) + + def pow_expr(self): + return self.getTypedRuleContext(ExprParser.Pow_exprContext, 0) + + def accept(self, visitor: ParseTreeVisitor): + if hasattr(visitor, "visitRightPower"): + return visitor.visitRightPower(self) + else: + return visitor.visitChildren(self) + + class RightPowAtomContext(Pow_exprContext): + + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.Pow_exprContext + super().__init__(parser) + self.copyFrom(ctx) + + def pow_atom(self): + return self.getTypedRuleContext(ExprParser.Pow_atomContext, 0) + + def accept(self, visitor: ParseTreeVisitor): + if hasattr(visitor, "visitRightPowAtom"): + return visitor.visitRightPowAtom(self) + else: + return visitor.visitChildren(self) + + def pow_expr(self): + + localctx = ExprParser.Pow_exprContext(self, self._ctx, self.state) + self.enterRule(localctx, 16, self.RULE_pow_expr) + 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.pow_atom() + self.state = 147 + self.match(ExprParser.T__1) + self.state = 148 + self.pow_expr() + pass + + elif la_ == 2: + localctx = ExprParser.RightPowAtomContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 150 + self.pow_atom() + pass + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class Pow_atomContext(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_pow_atom + + def copyFrom(self, ctx: ParserRuleContext): + super().copyFrom(ctx) + + class RightExpressionContext(Pow_atomContext): + + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.Pow_atomContext + 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(Pow_atomContext): + + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.Pow_atomContext + 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 pow_atom(self): + + localctx = ExprParser.Pow_atomContext(self, self._ctx, self.state) + self.enterRule(localctx, 18, self.RULE_pow_atom) + 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..c308d617 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#rightPow. + def visitRightPow(self, ctx: ExprParser.RightPowContext): 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#rightPowAtom. + def visitRightPowAtom(self, ctx: ExprParser.RightPowAtomContext): + 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 661ab69e..fda2fb86 100644 --- a/src/gems_craft/expression/parsing/parse_expression.py +++ b/src/gems_craft/expression/parsing/parse_expression.py @@ -99,6 +99,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 @@ -249,21 +255,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 power operand, not just to an atom, so + # that "t - 2^2" means "t - (2^2)" and not "t + ((-2)^2)". + operand = ctx.pow_expr().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( @@ -286,6 +287,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#rightPow. + def visitRightPow(self, ctx: ExprParser.RightPowContext) -> ExpressionNode: + return ctx.pow_expr().accept(self) # type: ignore + + # Visit a parse tree produced by ExprParser#rightPower. + def visitRightPower(self, ctx: ExprParser.RightPowerContext) -> ExpressionNode: + base = ctx.pow_atom().accept(self) # type: ignore + exponent = ctx.pow_expr().accept(self) # type: ignore + return base**exponent + + # Visit a parse tree produced by ExprParser#rightPowAtom. + def visitRightPowAtom(self, ctx: ExprParser.RightPowAtomContext) -> ExpressionNode: + return ctx.pow_atom().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 d90ef3c6..1f5bdb86 100644 --- a/src/gems_craft/expression/print.py +++ b/src/gems_craft/expression/print.py @@ -24,6 +24,7 @@ MinNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, TimeEvalNode, @@ -87,6 +88,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 7824da28..d9ded719 100644 --- a/src/gems_craft/expression/uses_sum_connections_on.py +++ b/src/gems_craft/expression/uses_sum_connections_on.py @@ -28,6 +28,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -62,6 +63,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 a7a15cc8..424e8c86 100644 --- a/src/gems_craft/expression/visitor.py +++ b/src/gems_craft/expression/visitor.py @@ -36,6 +36,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -72,6 +73,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: ... @@ -145,6 +149,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): @@ -182,7 +188,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 @@ -205,6 +211,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) @@ -212,7 +222,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: @@ -234,3 +244,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 bf6435d4..b6846e87 100644 --- a/src/gems_craft/model/port.py +++ b/src/gems_craft/model/port.py @@ -36,6 +36,7 @@ MinNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -149,6 +150,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 5326fe89..b19aeaf8 100644 --- a/src/gems_craft/model/resolve_library.py +++ b/src/gems_craft/model/resolve_library.py @@ -30,6 +30,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -233,6 +234,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 137bcc47..de3dc77f 100644 --- a/src/gems_runner/simulation/vectorized_builder.py +++ b/src/gems_runner/simulation/vectorized_builder.py @@ -58,6 +58,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + PowerNode, ReducedCostNode, RoundNode, ScenarioOperatorNode, @@ -364,6 +365,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] @@ -684,6 +690,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 ee430edf..497829e8 100644 --- a/tests/unittests/gems_craft/expressions/parsing/test_expression_parsing.py +++ b/tests/unittests/gems_craft/expressions/parsing/test_expression_parsing.py @@ -277,6 +277,86 @@ def test_parse_reduced_cost_unknown_variable_raises() -> None: parse_expression("reduced_cost(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", [ @@ -285,6 +365,9 @@ def test_parse_reduced_cost_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 0bceccea..de613103 100644 --- a/tests/unittests/gems_craft/expressions/visitor/test_degree.py +++ b/tests/unittests/gems_craft/expressions/visitor/test_degree.py @@ -95,3 +95,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_raises() -> None: + with pytest.raises( + ValueError, match="Exponent of a power expression must not depend on variables" + ): + visit(param("p") ** var("x"), ExpressionDegreeVisitor()) diff --git a/tests/unittests/gems_craft/expressions/visitor/test_equality.py b/tests/unittests/gems_craft/expressions/visitor/test_equality.py index 0c831a73..b153b490 100644 --- a/tests/unittests/gems_craft/expressions/visitor/test_equality.py +++ b/tests/unittests/gems_craft/expressions/visitor/test_equality.py @@ -37,6 +37,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: @@ -73,6 +75,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 2b6d71de..2a951c01 100644 --- a/tests/unittests/gems_craft/expressions/visitor/test_printer.py +++ b/tests/unittests/gems_craft/expressions/visitor/test_printer.py @@ -55,3 +55,13 @@ def test_abs_round_printer() -> None: def test_dual_reduced_cost_printer() -> None: assert visit(DualNode("balance"), PrinterVisitor()) == "dual(balance)" 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))" 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 e3930fb8..cad7712d 100644 --- a/tests/unittests/gems_craft/lib_parsing/test_lib_parsing.py +++ b/tests/unittests/gems_craft/lib_parsing/test_lib_parsing.py @@ -273,6 +273,72 @@ 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="Exponent of a power expression must not depend on variables" + ): + 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 72feee68..9204ba0e 100644 --- a/tests/unittests/gems_runner/expression/test_evaluation.py +++ b/tests/unittests/gems_runner/expression/test_evaluation.py @@ -28,6 +28,10 @@ ) from gems_craft.expression.equality import expressions_equal from gems_craft.expression.expression import DualNode, ReducedCostNode +from gems_craft.expression.parsing.parse_expression import ( + ModelIdentifiers, + parse_expression, +) from gems_runner.expression import EvaluationContext, EvaluationVisitor, ValueProvider @@ -110,3 +114,34 @@ def test_dual_reduced_cost_evaluation_raises() -> None: visit(DualNode("balance"), EvaluationVisitor(ctx)) with pytest.raises(NotImplementedError, match="reduced_cost"): 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) 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 f48aebd5..b567f3be 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 # --------------------------------------------------------------------------- From f2085398feea05f6ca02b7ddb2a5b11ab58b961a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 13:16:31 +0000 Subject: [PATCH 2/7] docs(changelog): condense the `^` operator entry Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015DqK4sz5N7o9FW3uyRjq6U --- docs/CHANGELOG.md | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6f448d6b..c2fdf8e7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,22 +9,12 @@ All notable changes to GemsPy are documented here. to **3.11** accordingly (linopy 0.9 requires Python >= 3.11). ### Added -- **Power operator `^` in the expression language** - `^` is now accepted in - constraints, objective contributions, variable bounds, port-field definitions - and extra-outputs, matching Antares Simulator. It is right-associative and - binds tighter than unary minus and than `*` `/`. Its operands must be - literals or parameters everywhere except in `extra-outputs`, where a decision - variable may be raised to a power (evaluated post-solve), like the existing - `floor` / `ceil` / `abs` / `round` / `min` / `max`. The exponent must never - depend on a decision variable. The Python API gains `**` on `ExpressionNode`. - See [Expression syntax](user-guide/expressions.md). - - **Divergence from Antares Simulator:** GemsPy parses `-2^2` as `-(2^2) = -4`, - the standard mathematical convention, whereas Antares Simulator currently - parses it as `(-2)^2 = 4`. The same model can therefore give different - results in the two interpreters until the Antares side is aligned. No - existing study is affected: `^` was previously a parse error, so no GEMS file - in circulation can contain it. +- **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 exponent must never depend on a variable. The Python + API gains `**` on `ExpressionNode`. See + [Expression syntax](user-guide/expressions.md). - **Integer strategy and thermal heuristics** - components can now set `integer-strategy` (`exact` (default), `relaxed`, or `heuristic` + From 8cc62830d575cc19beca1f55a7226c7b4b7a3115 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 13:19:57 +0000 Subject: [PATCH 3/7] docs: remove the expression syntax page Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015DqK4sz5N7o9FW3uyRjq6U --- docs/CHANGELOG.md | 3 +- docs/user-guide/expressions.md | 123 --------------------------------- mkdocs.yml | 1 - 3 files changed, 1 insertion(+), 126 deletions(-) delete mode 100644 docs/user-guide/expressions.md diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c2fdf8e7..39c65b40 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,8 +13,7 @@ All notable changes to GemsPy are documented here. 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 exponent must never depend on a variable. The Python - API gains `**` on `ExpressionNode`. See - [Expression syntax](user-guide/expressions.md). + API gains `**` on `ExpressionNode`. - **Integer strategy and thermal heuristics** - components can now set `integer-strategy` (`exact` (default), `relaxed`, or `heuristic` + diff --git a/docs/user-guide/expressions.md b/docs/user-guide/expressions.md deleted file mode 100644 index 353d41e6..00000000 --- a/docs/user-guide/expressions.md +++ /dev/null @@ -1,123 +0,0 @@ -# Expression syntax - -Constraints, objective contributions, variable bounds, port-field definitions -and extra-outputs are all written in the GEMS expression language. This page -documents the syntax **as the GemsPy interpreter accepts it**. For the language -itself, see the -[GEMS documentation](https://gems-energy.readthedocs.io/en/latest/user-guide/introduction/). - ---- - -## Operators - -| Operator | Meaning | Notes | -|---|---|---| -| `+` `-` | addition, subtraction | `-x` is also valid as unary minus | -| `*` `/` | multiplication, division | | -| `^` | power | right-associative, binds tighter than unary minus | -| `=` `<=` `>=` | comparison | only at the top level of a constraint | - -## Functions - -| Function | Meaning | -|---|---| -| `floor(e)` / `ceil(e)` | round down / up to the nearest integer | -| `round(e)` | round to the nearest integer (banker's rounding) | -| `abs(e)` | absolute value | -| `min(a, b, ...)` / `max(a, b, ...)` | element-wise minimum / maximum | -| `expec(e)` | expectation over scenarios | -| `sum(e)` | sum over all timesteps | -| `sum(t-a..t+b, e)` | sum over a time window | -| `sum_connections(port.field)` | sum of a port field over all connections | -| `dual(constraint_id)` | dual value of a constraint (extra-outputs only) | -| `reduced_cost(variable_id)` | reduced cost of a variable (extra-outputs only) | - -## Time indexing - -| Syntax | Meaning | -|---|---| -| `x[t]` | value at the current timestep (equivalent to `x`) | -| `x[t-1]`, `x[t+d]` | value at a timestep relative to the current one | -| `x[3]` | value at an absolute timestep | -| `(expr)[t-1]` | shift applied to a whole expression | - ---- - -## Precedence - -From tightest to loosest binding: - -1. `^` -2. unary `-` -3. `*` `/` -4. `+` `-` -5. `=` `<=` `>=` - -`^` is **right-associative**; all the other binary operators are -left-associative. Parentheses override precedence anywhere. - -### The power operator and unary minus - -`^` binds **tighter than unary minus**, following standard mathematical -notation: - -| Expression | Parses as | Value | -|---|---|---| -| `-2^2` | `-(2^2)` | `-4` | -| `2^-3` | `2^(-3)` | `0.125` | -| `2^3^2` | `2^(3^2)` | `512` | -| `2*3^2` | `2*(3^2)` | `18` | -| `2^3*2` | `(2^3)*2` | `16` | - -!!! warning "Divergence from Antares Simulator" - Antares Simulator currently parses `-2^2` as `(-2)^2 = 4`. GemsPy follows - the standard convention and evaluates it to `-4`. Write `(-2)^2` - explicitly if you need the same result in both interpreters. - -### Inside time-shift brackets - -Within `x[t ...]`, the sign applies to the whole power operand and `^` does not -swallow a trailing `*` or `/`: - -| Expression | Shift amount | -|---|---| -| `x[t-2^2]` | `-4` | -| `x[t-2^2*3]` | `-12` | -| `x[t-2*3^2]` | `-18` | - -A **signed exponent is not allowed** inside a time shift: `x[t+2^-1]` is a -parse error, since a fractional shift is meaningless. `2^-1` remains valid -everywhere else. - ---- - -## Where nonlinear expressions are allowed - -`^`, `floor`, `ceil`, `abs`, `round`, `min` and `max` may be applied to -**literals and parameters** anywhere. Applying them to a **decision variable** -produces a nonlinear expression, which is: - -- **rejected** in constraints, objective contributions and variable bounds, - with `Non-linear expression is not allowed in ...`; -- **allowed** in `extra-outputs`, which are evaluated after the solve. - -So `p^2`, `2^p` and `p^(1+q)` are valid in a constraint, `x^2` is not, and -`x^(2 + p)` is valid in an extra-output. - -The **exponent must never depend on a decision variable**, in any context: -`p^x` raises `Exponent of a power expression must not depend on variables.` - -~~~ yaml -constraints: - - id: capacity - expression: gen <= p_max^2 # parameters only — fine - -extra-outputs: - - id: squared_generation - expression: gen^2 # variable base — post-solve only -~~~ - -!!! note - `**` is not an alias for `^`. GEMS accepts `^` only, matching Antares - Simulator. (The GemsPy *Python* API does support `param("p") ** 2`, since - that is Python's own operator.) diff --git a/mkdocs.yml b/mkdocs.yml index bd1aa5e4..24e49165 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,6 @@ nav: - 'User guide: GEMS language and data structure': 'user-guide.md' - 'User guide: GemsPy interpreter package': - 'Reading input files': 'user-guide/inputs.md' - - 'Expression syntax': 'user-guide/expressions.md' - 'Building systems with the Python API': 'user-guide/building.md' - 'Optimization: building and solving problems': 'user-guide/optimisation.md' - 'Retrieving outputs': 'user-guide/outputs.md' From 660cd56c8511a2c2624e7c059013ccd9b78b23a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 18:52:36 +0000 Subject: [PATCH 4/7] fix(degree): return inf for a variable exponent instead of raising `ExpressionDegreeVisitor.power` raised a ValueError when the exponent depended on a variable, which made `is_linear` / `is_constant` partial: every caller treats them as plain booleans, so a port-field definition such as `2^g` aborted `resolve_system` instead of taking the non-linear branch, and `optimization.py` called them from inside an `except NotImplementedError` handler, chaining the two exceptions. Returning `math.inf` keeps the predicates total and aligns `^` with the other non-polynomial operators (`floor`, `ceil`, `abs`, `round`, `min`, `max`). A variable exponent in a constraint is still rejected, now via the usual "Non-linear expression is not allowed in ..." error, which names the offending context. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015DqK4sz5N7o9FW3uyRjq6U --- src/gems_craft/expression/degree.py | 9 +++------ .../gems_craft/expressions/visitor/test_degree.py | 11 ++++++----- .../gems_craft/lib_parsing/test_lib_parsing.py | 4 +--- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/gems_craft/expression/degree.py b/src/gems_craft/expression/degree.py index 14e3ea60..157b696d 100644 --- a/src/gems_craft/expression/degree.py +++ b/src/gems_craft/expression/degree.py @@ -76,13 +76,10 @@ def division(self, node: DivisionNode) -> int | float: return visit(node.left, self) def power(self, node: PowerNode) -> int | float: - # Mirrors the Antares Simulator rule: the exponent must not depend on - # variables, and a variable base only stays polynomial for a - # non-negative integer exponent. + # 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: - raise ValueError( - "Exponent of a power expression must not depend on variables." - ) + return math.inf base_degree = visit(node.left, self) if base_degree == 0: return 0 diff --git a/tests/unittests/gems_craft/expressions/visitor/test_degree.py b/tests/unittests/gems_craft/expressions/visitor/test_degree.py index de613103..87866a92 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, @@ -122,8 +123,8 @@ def test_power_degree() -> None: assert visit(FloorNode(x) ** 0, ExpressionDegreeVisitor()) == 0 -def test_variable_exponent_raises() -> None: - with pytest.raises( - ValueError, match="Exponent of a power expression must not depend on variables" - ): - visit(param("p") ** var("x"), ExpressionDegreeVisitor()) +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/lib_parsing/test_lib_parsing.py b/tests/unittests/gems_craft/lib_parsing/test_lib_parsing.py index cad7712d..9ad4e5a5 100644 --- a/tests/unittests/gems_craft/lib_parsing/test_lib_parsing.py +++ b/tests/unittests/gems_craft/lib_parsing/test_lib_parsing.py @@ -333,9 +333,7 @@ def test_variable_exponent_in_constraint_is_rejected() -> None: expression: p^x = 0 """) input_lib = parse_yaml_library(lib_yaml) - with pytest.raises( - ValueError, match="Exponent of a power expression must not depend on variables" - ): + with pytest.raises(ValueError, match="Non-linear expression is not allowed"): resolve_library([input_lib]) From e74bc3292816ac68502f690e575230e1788d73d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 18:59:16 +0000 Subject: [PATCH 5/7] docs(changelog): drop the redundant exponent rule A variable-dependent exponent is no longer a rule of its own: it yields an infinite degree, so it is covered by the existing constraint that constraints, bounds and objective contributions be linear. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015DqK4sz5N7o9FW3uyRjq6U --- docs/CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 39c65b40..2d0898e4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,8 +12,7 @@ All notable changes to GemsPy are documented here. - **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 exponent must never depend on a variable. The Python - API gains `**` on `ExpressionNode`. + be raised to a power. The Python API gains `**` on `ExpressionNode`. - **Integer strategy and thermal heuristics** - components can now set `integer-strategy` (`exact` (default), `relaxed`, or `heuristic` + From 4119ea79fb9dbb158d4cfcb2a927d75273e77263 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 08:44:34 +0000 Subject: [PATCH 6/7] refactor(grammar): name the shift tiers after their level, not after `^` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pow_expr` / `pow_atom` are the two highest precedence tiers of the shift sub-grammar, not power-specific rules: every operand of a time shift is dispatched through them whether or not `^` is involved, so a bare `3` in `x[t-3]` went through `pow_expr -> pow_atom -> atom`. Renames them to `shift_operand` / `shift_primary`, and the two pass-through labels `rightPow` / `rightPowAtom` to `rightOperand` / `rightPrimary`. `rightExpression`, `rightAtom` and `rightPower` keep their labels, so those visitor methods are untouched. Behaviour is identical: the regenerated automaton is unchanged (only rule and context names move), and the comment now records why the tiers cannot be folded into `right_expr` — the leading sign of a shift needs an operand that accepts `^` but not `*` and `/`, otherwise `t - 2*3` becomes ambiguous and reassociates to `-(2*3)`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015DqK4sz5N7o9FW3uyRjq6U --- grammar/Expr.g4 | 28 ++++-- .../expression/parsing/antlr/Expr.interp | 4 +- .../expression/parsing/antlr/ExprParser.py | 94 +++++++++---------- .../expression/parsing/antlr/ExprVisitor.py | 8 +- .../expression/parsing/parse_expression.py | 20 ++-- 5 files changed, 81 insertions(+), 73 deletions(-) diff --git a/grammar/Expr.g4 b/grammar/Expr.g4 index 017ad361..21ef0e3d 100644 --- a/grammar/Expr.g4 +++ b/grammar/Expr.g4 @@ -61,27 +61,35 @@ shift: TIME shift_expr?; shift_expr : shift_expr op=('*' | '/') right_expr # shiftMuldiv | shift_expr op=('+' | '-') right_expr # shiftAddsub - | op=('+' | '-') pow_expr # signedOperand + | op=('+' | '-') shift_operand # signedOperand ; right_expr : right_expr op=('/' | '*') right_expr # rightMuldiv - | pow_expr # rightPow + | shift_operand # rightOperand ; -// Operand of '^' inside a time shift, chained right-associatively. -// Deliberately NOT "right_expr" on the right of '^': being a separate rule, -// it would be entered at precedence 0 and greedily swallow '*' and '/', -// so "t - 2^2*3" would mean "2^(2*3)" instead of "(2^2)*3". +// The two highest precedence tiers of the shift sub-grammar. 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: +// - the leading sign of "shift_expr" needs a power-capable operand that does +// NOT also swallow '*' and '/', otherwise "t - 2*3" becomes ambiguous and +// reassociates to "-(2*3)" instead of "(-2)*3"; +// - "right_expr" on the right of '^' would be entered at precedence 0 and +// greedily swallow '*' and '/', so "t - 2^2*3" would mean "2^(2*3)" +// instead of "(2^2)*3". +// // The exponent is unsigned here, so "t + 2^-1" is a parse error: a fractional // time shift is meaningless, and negative exponents remain available in the // general "expr" rule. -pow_expr - : pow_atom '^' pow_expr # rightPower - | pow_atom # rightPowAtom +shift_operand + : shift_primary '^' shift_operand # rightPower + | shift_primary # rightPrimary ; -pow_atom +shift_primary : '(' expr ')' # rightExpression | atom # rightAtom ; diff --git a/src/gems_craft/expression/parsing/antlr/Expr.interp b/src/gems_craft/expression/parsing/antlr/Expr.interp index b0092611..8d302381 100644 --- a/src/gems_craft/expression/parsing/antlr/Expr.interp +++ b/src/gems_craft/expression/parsing/antlr/Expr.interp @@ -51,8 +51,8 @@ atom shift shift_expr right_expr -pow_expr -pow_atom +shift_operand +shift_primary atn: diff --git a/src/gems_craft/expression/parsing/antlr/ExprParser.py b/src/gems_craft/expression/parsing/antlr/ExprParser.py index 2bfdd10b..e3be8e15 100644 --- a/src/gems_craft/expression/parsing/antlr/ExprParser.py +++ b/src/gems_craft/expression/parsing/antlr/ExprParser.py @@ -1503,8 +1503,8 @@ class ExprParser(Parser): RULE_shift = 5 RULE_shift_expr = 6 RULE_right_expr = 7 - RULE_pow_expr = 8 - RULE_pow_atom = 9 + RULE_shift_operand = 8 + RULE_shift_primary = 9 ruleNames = [ "portFieldExpr", @@ -1515,8 +1515,8 @@ class ExprParser(Parser): "shift", "shift_expr", "right_expr", - "pow_expr", - "pow_atom", + "shift_operand", + "shift_primary", ] EOF = Token.EOF @@ -2509,8 +2509,8 @@ def __init__( self.op = None # Token self.copyFrom(ctx) - def pow_expr(self): - return self.getTypedRuleContext(ExprParser.Pow_exprContext, 0) + def shift_operand(self): + return self.getTypedRuleContext(ExprParser.Shift_operandContext, 0) def accept(self, visitor: ParseTreeVisitor): if hasattr(visitor, "visitSignedOperand"): @@ -2541,7 +2541,7 @@ def shift_expr(self, _p: int = 0): self._errHandler.reportMatch(self) self.consume() self.state = 122 - self.pow_expr() + self.shift_operand() self._ctx.stop = self._input.LT(-1) self.state = 132 self._errHandler.sync(self) @@ -2639,7 +2639,7 @@ def getRuleIndex(self): def copyFrom(self, ctx: ParserRuleContext): super().copyFrom(ctx) - class RightPowContext(Right_exprContext): + class RightOperandContext(Right_exprContext): def __init__( self, parser, ctx: ParserRuleContext @@ -2647,12 +2647,12 @@ def __init__( super().__init__(parser) self.copyFrom(ctx) - def pow_expr(self): - return self.getTypedRuleContext(ExprParser.Pow_exprContext, 0) + def shift_operand(self): + return self.getTypedRuleContext(ExprParser.Shift_operandContext, 0) def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitRightPow"): - return visitor.visitRightPow(self) + if hasattr(visitor, "visitRightOperand"): + return visitor.visitRightOperand(self) else: return visitor.visitChildren(self) @@ -2687,12 +2687,12 @@ def right_expr(self, _p: int = 0): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - localctx = ExprParser.RightPowContext(self, localctx) + localctx = ExprParser.RightOperandContext(self, localctx) self._ctx = localctx _prevctx = localctx self.state = 136 - self.pow_expr() + self.shift_operand() self._ctx.stop = self._input.LT(-1) self.state = 143 self._errHandler.sync(self) @@ -2738,7 +2738,7 @@ def right_expr(self, _p: int = 0): self.unrollRecursionContexts(_parentctx) return localctx - class Pow_exprContext(ParserRuleContext): + class Shift_operandContext(ParserRuleContext): __slots__ = "parser" def __init__( @@ -2748,52 +2748,52 @@ def __init__( self.parser = parser def getRuleIndex(self): - return ExprParser.RULE_pow_expr + return ExprParser.RULE_shift_operand def copyFrom(self, ctx: ParserRuleContext): super().copyFrom(ctx) - class RightPowerContext(Pow_exprContext): + class RightPrimaryContext(Shift_operandContext): def __init__( self, parser, ctx: ParserRuleContext - ): # actually a ExprParser.Pow_exprContext + ): # actually a ExprParser.Shift_operandContext super().__init__(parser) self.copyFrom(ctx) - def pow_atom(self): - return self.getTypedRuleContext(ExprParser.Pow_atomContext, 0) - - def pow_expr(self): - return self.getTypedRuleContext(ExprParser.Pow_exprContext, 0) + def shift_primary(self): + return self.getTypedRuleContext(ExprParser.Shift_primaryContext, 0) def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitRightPower"): - return visitor.visitRightPower(self) + if hasattr(visitor, "visitRightPrimary"): + return visitor.visitRightPrimary(self) else: return visitor.visitChildren(self) - class RightPowAtomContext(Pow_exprContext): + class RightPowerContext(Shift_operandContext): def __init__( self, parser, ctx: ParserRuleContext - ): # actually a ExprParser.Pow_exprContext + ): # actually a ExprParser.Shift_operandContext super().__init__(parser) self.copyFrom(ctx) - def pow_atom(self): - return self.getTypedRuleContext(ExprParser.Pow_atomContext, 0) + 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, "visitRightPowAtom"): - return visitor.visitRightPowAtom(self) + if hasattr(visitor, "visitRightPower"): + return visitor.visitRightPower(self) else: return visitor.visitChildren(self) - def pow_expr(self): + def shift_operand(self): - localctx = ExprParser.Pow_exprContext(self, self._ctx, self.state) - self.enterRule(localctx, 16, self.RULE_pow_expr) + 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) @@ -2802,18 +2802,18 @@ def pow_expr(self): localctx = ExprParser.RightPowerContext(self, localctx) self.enterOuterAlt(localctx, 1) self.state = 146 - self.pow_atom() + self.shift_primary() self.state = 147 self.match(ExprParser.T__1) self.state = 148 - self.pow_expr() + self.shift_operand() pass elif la_ == 2: - localctx = ExprParser.RightPowAtomContext(self, localctx) + localctx = ExprParser.RightPrimaryContext(self, localctx) self.enterOuterAlt(localctx, 2) self.state = 150 - self.pow_atom() + self.shift_primary() pass except RecognitionException as re: @@ -2824,7 +2824,7 @@ def pow_expr(self): self.exitRule() return localctx - class Pow_atomContext(ParserRuleContext): + class Shift_primaryContext(ParserRuleContext): __slots__ = "parser" def __init__( @@ -2834,16 +2834,16 @@ def __init__( self.parser = parser def getRuleIndex(self): - return ExprParser.RULE_pow_atom + return ExprParser.RULE_shift_primary def copyFrom(self, ctx: ParserRuleContext): super().copyFrom(ctx) - class RightExpressionContext(Pow_atomContext): + class RightExpressionContext(Shift_primaryContext): def __init__( self, parser, ctx: ParserRuleContext - ): # actually a ExprParser.Pow_atomContext + ): # actually a ExprParser.Shift_primaryContext super().__init__(parser) self.copyFrom(ctx) @@ -2856,11 +2856,11 @@ def accept(self, visitor: ParseTreeVisitor): else: return visitor.visitChildren(self) - class RightAtomContext(Pow_atomContext): + class RightAtomContext(Shift_primaryContext): def __init__( self, parser, ctx: ParserRuleContext - ): # actually a ExprParser.Pow_atomContext + ): # actually a ExprParser.Shift_primaryContext super().__init__(parser) self.copyFrom(ctx) @@ -2873,10 +2873,10 @@ def accept(self, visitor: ParseTreeVisitor): else: return visitor.visitChildren(self) - def pow_atom(self): + def shift_primary(self): - localctx = ExprParser.Pow_atomContext(self, self._ctx, self.state) - self.enterRule(localctx, 18, self.RULE_pow_atom) + 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) diff --git a/src/gems_craft/expression/parsing/antlr/ExprVisitor.py b/src/gems_craft/expression/parsing/antlr/ExprVisitor.py index c308d617..ca09b432 100644 --- a/src/gems_craft/expression/parsing/antlr/ExprVisitor.py +++ b/src/gems_craft/expression/parsing/antlr/ExprVisitor.py @@ -111,8 +111,8 @@ def visitShiftAddsub(self, ctx: ExprParser.ShiftAddsubContext): def visitSignedOperand(self, ctx: ExprParser.SignedOperandContext): return self.visitChildren(ctx) - # Visit a parse tree produced by ExprParser#rightPow. - def visitRightPow(self, ctx: ExprParser.RightPowContext): + # 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. @@ -123,8 +123,8 @@ def visitRightMuldiv(self, ctx: ExprParser.RightMuldivContext): def visitRightPower(self, ctx: ExprParser.RightPowerContext): return self.visitChildren(ctx) - # Visit a parse tree produced by ExprParser#rightPowAtom. - def visitRightPowAtom(self, ctx: ExprParser.RightPowAtomContext): + # 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. diff --git a/src/gems_craft/expression/parsing/parse_expression.py b/src/gems_craft/expression/parsing/parse_expression.py index fda2fb86..abab96e7 100644 --- a/src/gems_craft/expression/parsing/parse_expression.py +++ b/src/gems_craft/expression/parsing/parse_expression.py @@ -259,9 +259,9 @@ def visitShiftMuldiv(self, ctx: ExprParser.ShiftMuldivContext) -> ExpressionNode def visitSignedOperand( self, ctx: ExprParser.SignedOperandContext ) -> ExpressionNode: - # The sign applies to a whole power operand, not just to an atom, so + # 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.pow_expr().accept(self) # type: ignore + operand = ctx.shift_operand().accept(self) # type: ignore if ctx.op.text == "-": # type: ignore return -operand return operand @@ -287,19 +287,19 @@ 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#rightPow. - def visitRightPow(self, ctx: ExprParser.RightPowContext) -> ExpressionNode: - return ctx.pow_expr().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.pow_atom().accept(self) # type: ignore - exponent = ctx.pow_expr().accept(self) # type: ignore + 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#rightPowAtom. - def visitRightPowAtom(self, ctx: ExprParser.RightPowAtomContext) -> ExpressionNode: - return ctx.pow_atom().accept(self) # type: ignore + # 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 = { From b0674b8579113409fa3d790cc1158d11616733b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 14:47:24 +0000 Subject: [PATCH 7/7] Clarify the shift sub-grammar comments about '^' and the leading sign State up front that '^' means inside a time shift exactly what it means in a general expression, and that the extra rules exist only because the shift sub-grammar is a hand-written cascade: precedence lives in which rule each operand recurses into, not in the order of the alternatives, so an operator added to "expr" gets nothing inside a shift until it is restated there. Each reason for keeping shift_operand/shift_primary separate from right_expr now names the "expr" behaviour it reproduces, and the unsigned exponent is called out as the one deliberate divergence rather than one more item in the same list. Comments only; the generated parser is unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KouPcoHJZyxHurBzHqYSLk --- grammar/Expr.g4 | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/grammar/Expr.g4 b/grammar/Expr.g4 index 21ef0e3d..a889931d 100644 --- a/grammar/Expr.g4 +++ b/grammar/Expr.g4 @@ -54,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 @@ -69,21 +78,25 @@ right_expr | shift_operand # rightOperand ; -// The two highest precedence tiers of the shift sub-grammar. They are named -// after their level, not after '^': every operand of a shift goes through -// them, whether or not a power is involved. +// 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: -// - the leading sign of "shift_expr" needs a power-capable operand that does -// NOT also swallow '*' and '/', otherwise "t - 2*3" becomes ambiguous and -// reassociates to "-(2*3)" instead of "(-2)*3"; -// - "right_expr" on the right of '^' would be entered at precedence 0 and -// greedily swallow '*' and '/', so "t - 2^2*3" would mean "2^(2*3)" -// instead of "(2^2)*3". +// 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 exponent is unsigned here, so "t + 2^-1" is a parse error: a fractional -// time shift is meaningless, and negative exponents remain available in the -// general "expr" rule. +// 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