Coverage for python/lsst/daf/butler/registry/queries/expressions/normalForm.py: 41%
260 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-28 04:40 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-28 04:40 -0700
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22from __future__ import annotations
24__all__ = (
25 "NormalForm",
26 "NormalFormExpression",
27 "NormalFormVisitor",
28)
30import enum
31from abc import ABC, abstractmethod
32from typing import Dict, Generic, Iterator, List, Optional, Sequence, Tuple, TypeVar
34import astropy.time
36from .parser import BinaryOp, Node, Parens, TreeVisitor, UnaryOp
39class LogicalBinaryOperator(enum.Enum):
40 """Enumeration for logical binary operators.
42 These intentionally have the same names (including capitalization) as the
43 string binary operators used in the parser itself. Boolean values are
44 used to enable generic code that works on either operator (particularly
45 ``LogicalBinaryOperator(not self)`` as a way to get the other operator).
47 Which is `True` and which is `False` is just a convention, but one shared
48 by `LogicalBinaryOperator` and `NormalForm`.
49 """
51 AND = True
52 OR = False
54 def apply(self, lhs: TransformationWrapper, rhs: TransformationWrapper) -> LogicalBinaryOperation:
55 """Return a `TransformationWrapper` object representing this operator
56 applied to the given operands.
58 This is simply syntactic sugar for the `LogicalBinaryOperation`
59 constructor.
61 Parameters
62 ----------
63 lhs: `TransformationWrapper`
64 First operand.
65 rhs: `TransformationWrapper`
66 Second operand.
68 Returns
69 -------
70 operation : `LogicalBinaryOperation`
71 Object representing the operation.
72 """
73 return LogicalBinaryOperation(lhs, self, rhs)
76class NormalForm(enum.Enum):
77 """Enumeration for boolean normal forms.
79 Both normal forms require all NOT operands to be moved "inside" any AND
80 or OR operations (i.e. by applying DeMorgan's laws), with either all AND
81 operations or all OR operations appearing outside the other (`CONJUNCTIVE`,
82 and `DISJUNCTIVE`, respectively).
84 Boolean values are used here to enable generic code that works on either
85 form, and for interoperability with `LogicalBinaryOperator`.
87 Which is `True` and which is `False` is just a convention, but one shared
88 by `LogicalBinaryOperator` and `NormalForm`.
89 """
91 CONJUNCTIVE = True
92 """Form in which AND operations may have OR operands, but not the reverse.
94 For example, ``A AND (B OR C)`` is in conjunctive normal form, but
95 ``A OR (B AND C)`` and ``A AND (B OR (C AND D))`` are not.
96 """
98 DISJUNCTIVE = False
99 """Form in which OR operations may have AND operands, but not the reverse.
101 For example, ``A OR (B AND C)`` is in disjunctive normal form, but
102 ``A AND (B OR C)`` and ``A OR (B AND (C OR D))`` are not.
103 """
105 @property
106 def inner(self) -> LogicalBinaryOperator:
107 """The operator that is not permitted to contain operations of the
108 other type in this form.
110 Note that this operation may still appear as the outermost operator in
111 an expression in this form if there are no operations of the other
112 type.
113 """
114 return LogicalBinaryOperator(not self.value)
116 @property
117 def outer(self) -> LogicalBinaryOperator:
118 """The operator that is not permitted to be contained by operations of
119 the other type in this form.
121 Note that an operation of this type is not necessarily the outermost
122 operator in an expression in this form; the expression need not contain
123 any operations of this type.
124 """
125 return LogicalBinaryOperator(self.value)
127 def allows(self, *, inner: LogicalBinaryOperator, outer: LogicalBinaryOperator) -> bool:
128 """Test whether this form allows the given operator relationships.
130 Parameters
131 ----------
132 inner : `LogicalBinaryOperator`
133 Inner operator in an expression. May be the same as ``outer``.
134 outer : `LogicalBinaryOperator`
135 Outer operator in an expression. May be the same as ``inner``.
137 Returns
138 -------
139 allowed : `bool`
140 Whether this operation relationship is allowed.
141 """
142 return inner == outer or outer is self.outer
145_T = TypeVar("_T")
146_U = TypeVar("_U")
147_V = TypeVar("_V")
150class NormalFormVisitor(Generic[_T, _U, _V]):
151 """A visitor interface for `NormalFormExpression`.
153 A visitor implementation may inherit from both `NormalFormVisitor` and
154 `TreeVisitor` and implement `visitBranch` as ``node.visit(self)`` to
155 visit each node of the entire tree (or similarly with composition, etc).
156 In this case, `TreeVisitor.visitBinaryOp` will never be called with a
157 logical OR or AND operation, because these will all have been moved into
158 calls to `visitInner` and `visitOuter` instead.
160 See also `NormalFormExpression.visit`.
161 """
163 @abstractmethod
164 def visitBranch(self, node: Node) -> _T:
165 """Visit a regular `Node` and its child nodes.
167 Parameters
168 ----------
169 node : `Node`
170 A branch of the expression tree that contains no AND or OR
171 operations.
173 Returns
174 -------
175 result
176 Implementation-defined result to be gathered and passed to
177 `visitInner`.
178 """
179 raise NotImplementedError()
181 @abstractmethod
182 def visitInner(self, branches: Sequence[_T], form: NormalForm) -> _U:
183 """Visit a sequence of inner OR (for `~NormalForm.CONJUNCTIVE` form)
184 or AND (for `~NormalForm.DISJUNCTIVE`) operands.
186 Parameters
187 ----------
188 branches : `Sequence`
189 Sequence of tuples, where the first element in each tuple is the
190 result of a call to `visitBranch`, and the second is the `Node` on
191 which `visitBranch` was called.
192 form : `NormalForm`
193 Form this expression is in. ``form.inner`` is the operator that
194 joins the operands in ``branches``.
196 Returns
197 -------
198 result
199 Implementation-defined result to be gathered and passed to
200 `visitOuter`.
201 """
202 raise NotImplementedError()
204 @abstractmethod
205 def visitOuter(self, branches: Sequence[_U], form: NormalForm) -> _V:
206 """Visit the sequence of outer AND (for `~NormalForm.CONJUNCTIVE` form)
207 or OR (for `~NormalForm.DISJUNCTIVE`) operands.
209 Parameters
210 ----------
211 branches : `Sequence`
212 Sequence of return values from calls to `visitInner`.
213 form : `NormalForm`
214 Form this expression is in. ``form.outer`` is the operator that
215 joins the operands in ``branches``.
217 Returns
218 -------
219 result
220 Implementation-defined result to be returned by
221 `NormalFormExpression.visitNormalForm`.
222 """
223 raise NotImplementedError()
226class NormalFormExpression:
227 """A boolean expression in a standard normal form.
229 Most code should use `fromTree` to construct new instances instead of
230 calling the constructor directly. See `NormalForm` for a description of
231 the two forms.
233 Parameters
234 ----------
235 nodes : `Sequence` [ `Sequence` [ `Node` ] ]
236 Non-AND, non-OR branches of three tree, with the AND and OR operations
237 combining them represented by position in the nested sequence - the
238 inner sequence is combined via ``form.inner``, and the outer sequence
239 combines those via ``form.outer``.
240 form : `NormalForm`
241 Enumeration value indicating the form this expression is in.
242 """
244 def __init__(self, nodes: Sequence[Sequence[Node]], form: NormalForm):
245 self._form = form
246 self._nodes = nodes
248 def __str__(self) -> str:
249 return str(self.toTree())
251 @staticmethod
252 def fromTree(root: Node, form: NormalForm) -> NormalFormExpression:
253 """Construct a `NormalFormExpression` by normalizing an arbitrary
254 expression tree.
256 Parameters
257 ----------
258 root : `Node`
259 Root of the tree to be normalized.
260 form : `NormalForm`
261 Enumeration value indicating the form to normalize to.
263 Notes
264 -----
265 Converting an arbitrary boolean expression to either normal form is an
266 NP-hard problem, and this is a brute-force algorithm. I'm not sure
267 what its actual algorithmic scaling is, but it'd definitely be a bad
268 idea to attempt to normalize an expression with hundreds or thousands
269 of clauses.
270 """
271 wrapper = root.visit(TransformationVisitor()).normalize(form)
272 nodes = []
273 for outerOperands in wrapper.flatten(form.outer):
274 nodes.append([w.unwrap() for w in outerOperands.flatten(form.inner)])
275 return NormalFormExpression(nodes, form=form)
277 @property
278 def form(self) -> NormalForm:
279 """Enumeration value indicating the form this expression is in."""
280 return self._form
282 def visit(self, visitor: NormalFormVisitor[_T, _U, _V]) -> _V:
283 """Apply a visitor to the expression, explicitly taking advantage of
284 the special structure guaranteed by the normal forms.
286 Parameters
287 ----------
288 visitor : `NormalFormVisitor`
289 Visitor object to apply.
291 Returns
292 -------
293 result
294 Return value from calling ``visitor.visitOuter`` after visiting
295 all other nodes.
296 """
297 visitedOuterBranches: List[_U] = []
298 for nodeInnerBranches in self._nodes:
299 visitedInnerBranches = [visitor.visitBranch(node) for node in nodeInnerBranches]
300 visitedOuterBranches.append(visitor.visitInner(visitedInnerBranches, self.form))
301 return visitor.visitOuter(visitedOuterBranches, self.form)
303 def toTree(self) -> Node:
304 """Convert ``self`` back into an equivalent tree, preserving the
305 form of the boolean expression but representing it in memory as
306 a tree.
308 Returns
309 -------
310 tree : `Node`
311 Root of the tree equivalent to ``self``.
312 """
313 visitor = TreeReconstructionVisitor()
314 return self.visit(visitor)
317class PrecedenceTier(enum.Enum):
318 """An enumeration used to track operator precedence.
320 This enum is currently used only to inject parentheses into boolean
321 logic that has been manipulated into a new form, and because those
322 parentheses are only used for stringification, the goal here is human
323 readability, not precision.
325 Lower enum values represent tighter binding, but code deciding whether to
326 inject parentheses should always call `needsParens` instead of comparing
327 values directly.
328 """
330 TOKEN = 0
331 """Precedence tier for literals, identifiers, and expressions already in
332 parentheses.
333 """
335 UNARY = 1
336 """Precedence tier for unary operators, which always bind more tightly
337 than any binary operator.
338 """
340 VALUE_BINARY_OP = 2
341 """Precedence tier for binary operators that return non-boolean values.
342 """
344 COMPARISON = 3
345 """Precedence tier for binary comparison operators that accept non-boolean
346 values and return boolean values.
347 """
349 AND = 4
350 """Precedence tier for logical AND.
351 """
353 OR = 5
354 """Precedence tier for logical OR.
355 """
357 @classmethod
358 def needsParens(cls, outer: PrecedenceTier, inner: PrecedenceTier) -> bool:
359 """Test whether parentheses should be added around an operand.
361 Parameters
362 ----------
363 outer : `PrecedenceTier`
364 Precedence tier for the operation.
365 inner : `PrecedenceTier`
366 Precedence tier for the operand.
368 Returns
369 -------
370 needed : `bool`
371 If `True`, parentheses should be added.
373 Notes
374 -----
375 This method special cases logical binary operators for readability,
376 adding parentheses around ANDs embedded in ORs, and avoiding them in
377 chains of the same operator. If it is ever actually used with other
378 binary operators as ``outer``, those would probably merit similar
379 attention (or a totally new approach); its current approach would
380 aggressively add a lot of unfortunate parentheses because (aside from
381 this special-casing) it doesn't know about commutativity.
383 In fact, this method is rarely actually used for logical binary
384 operators either; the `LogicalBinaryOperation.unwrap` method that calls
385 it is never invoked by `NormalFormExpression` (the main public
386 interface), because those operators are flattened out (see
387 `TransformationWrapper.flatten`) instead. Parentheses are instead
388 added there by `TreeReconstructionVisitor`, which is simpler because
389 the structure of operators is restricted.
390 """
391 if outer is cls.OR and inner is cls.AND:
392 return True
393 if outer is cls.OR and inner is cls.OR:
394 return False
395 if outer is cls.AND and inner is cls.AND:
396 return False
397 return outer.value <= inner.value
400BINARY_OPERATOR_PRECEDENCE = {
401 "=": PrecedenceTier.COMPARISON,
402 "!=": PrecedenceTier.COMPARISON,
403 "<": PrecedenceTier.COMPARISON,
404 "<=": PrecedenceTier.COMPARISON,
405 ">": PrecedenceTier.COMPARISON,
406 ">=": PrecedenceTier.COMPARISON,
407 "OVERLAPS": PrecedenceTier.COMPARISON,
408 "+": PrecedenceTier.VALUE_BINARY_OP,
409 "-": PrecedenceTier.VALUE_BINARY_OP,
410 "*": PrecedenceTier.VALUE_BINARY_OP,
411 "/": PrecedenceTier.VALUE_BINARY_OP,
412 "AND": PrecedenceTier.AND,
413 "OR": PrecedenceTier.OR,
414}
417class TransformationWrapper(ABC):
418 """A base class for `Node` wrappers that can be used to transform boolean
419 operator expressions.
421 Notes
422 -----
423 `TransformationWrapper` instances should only be directly constructed by
424 each other or `TransformationVisitor`. No new subclasses beyond those in
425 this module should be added.
427 While `TransformationWrapper` and its subclasses contain the machinery
428 for transforming expressions to normal forms, it does not provide a
429 convenient interface for working with expressions in those forms; most code
430 should just use `NormalFormExpression` (which delegates to
431 `TransformationWrapper` internally) instead.
432 """
434 __slots__ = ()
436 @abstractmethod
437 def __str__(self) -> str:
438 raise NotImplementedError()
440 @property
441 @abstractmethod
442 def precedence(self) -> PrecedenceTier:
443 """Return the precedence tier for this node (`PrecedenceTier`).
445 Notes
446 -----
447 This is only used when reconstructing a full `Node` tree in `unwrap`
448 implementations, and only to inject parenthesis that are necessary for
449 correct stringification but nothing else (because the tree structure
450 itself embeds the correct order of operations already).
451 """
452 raise NotImplementedError()
454 @abstractmethod
455 def not_(self) -> TransformationWrapper:
456 """Return a wrapper that represents the logical NOT of ``self``.
458 Returns
459 -------
460 wrapper : `TransformationWrapper`
461 A wrapper that represents the logical NOT of ``self``.
462 """
463 raise NotImplementedError()
465 def satisfies(self, form: NormalForm) -> bool:
466 """Test whether this expressions is already in a normal form.
468 The default implementation is appropriate for "atomic" classes that
469 never contain any AND or OR operations at all.
471 Parameters
472 ----------
473 form : `NormalForm`
474 Enumeration indicating the form to test for.
476 Returns
477 -------
478 satisfies : `bool`
479 Whether ``self`` satisfies the requirements of ``form``.
480 """
481 return True
483 def normalize(self, form: NormalForm) -> TransformationWrapper:
484 """Return an expression equivalent to ``self`` in a normal form.
486 The default implementation is appropriate for "atomic" classes that
487 never contain any AND or OR operations at all.
489 Parameters
490 ----------
491 form : `NormalForm`
492 Enumeration indicating the form to convert to.
494 Returns
495 -------
496 normalized : `TransformationWrapper`
497 An expression equivalent to ``self`` for which
498 ``normalized.satisfies(form)`` returns `True`.
500 Notes
501 -----
502 Converting an arbitrary boolean expression to either normal form is an
503 NP-hard problem, and this is a brute-force algorithm. I'm not sure
504 what its actual algorithmic scaling is, but it'd definitely be a bad
505 idea to attempt to normalize an expression with hundreds or thousands
506 of clauses.
507 """
508 return self
510 def flatten(self, operator: LogicalBinaryOperator) -> Iterator[TransformationWrapper]:
511 """Recursively flatten the operands of any nested operators of the
512 given type.
514 For an expression like ``(A AND ((B OR C) AND D)`` (with
515 ``operator == AND``), `flatten` yields ``A, (B OR C), D``.
517 The default implementation is appropriate for "atomic" classes that
518 never contain any AND or OR operations at all.
520 Parameters
521 ----------
522 operator : `LogicalBinaryOperator`
523 Operator whose operands to flatten.
525 Returns
526 -------
527 operands : `Iterator` [ `TransformationWrapper` ]
528 Operands that, if combined with ``operator``, yield an expression
529 equivalent to ``self``.
530 """
531 yield self
533 def _satisfiesDispatch(
534 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
535 ) -> bool:
536 """Test whether ``operator.apply(self, other)`` is in a normal form.
538 The default implementation is appropriate for classes that never
539 contain any AND or OR operations at all.
541 Parameters
542 ----------
543 operator : `LogicalBinaryOperator`
544 Operator for the operation being tested.
545 other : `TransformationWrapper`
546 Other operand for the operation being tested.
547 form : `NormalForm`
548 Normal form being tested for.
550 Returns
551 -------
552 satisfies : `bool`
553 Whether ``operator.apply(self, other)`` satisfies the requirements
554 of ``form``.
556 Notes
557 -----
558 Caller guarantees that ``self`` and ``other`` are already normalized.
559 """
560 return other._satisfiesDispatchAtomic(operator, self, form=form)
562 def _normalizeDispatch(
563 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
564 ) -> TransformationWrapper:
565 """Return an expression equivalent to ``operator.apply(self, other)``
566 in a normal form.
568 The default implementation is appropriate for classes that never
569 contain any AND or OR operations at all.
571 Parameters
572 ----------
573 operator : `LogicalBinaryOperator`
574 Operator for the operation being transformed.
575 other : `TransformationWrapper`
576 Other operand for the operation being transformed.
577 form : `NormalForm`
578 Normal form being transformed to.
580 Returns
581 -------
582 normalized : `TransformationWrapper`
583 An expression equivalent to ``operator.apply(self, other)``
584 for which ``normalized.satisfies(form)`` returns `True`.
586 Notes
587 -----
588 Caller guarantees that ``self`` and ``other`` are already normalized.
589 """
590 return other._normalizeDispatchAtomic(operator, self, form=form)
592 def _satisfiesDispatchAtomic(
593 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
594 ) -> bool:
595 """Test whather ``operator.apply(other, self)`` is in a normal form.
597 The default implementation is appropriate for "atomic" classes that
598 never contain any AND or OR operations at all.
600 Parameters
601 ----------
602 operator : `LogicalBinaryOperator`
603 Operator for the operation being tested.
604 other : `TransformationWrapper`
605 Other operand for the operation being tested.
606 form : `NormalForm`
607 Normal form being tested for.
609 Returns
610 -------
611 satisfies : `bool`
612 Whether ``operator.apply(other, self)`` satisfies the requirements
613 of ``form``.
615 Notes
616 -----
617 Should only be called by `_satisfiesDispatch` implementations; this
618 guarantees that ``other`` is atomic and ``self`` is normalized.
619 """
620 return True
622 def _normalizeDispatchAtomic(
623 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
624 ) -> TransformationWrapper:
625 """Return an expression equivalent to ``operator.apply(other, self)``,
626 in a normal form.
628 The default implementation is appropriate for "atomic" classes that
629 never contain any AND or OR operations at all.
631 Parameters
632 ----------
633 operator : `LogicalBinaryOperator`
634 Operator for the operation being transformed.
635 other : `TransformationWrapper`
636 Other operand for the operation being transformed.
637 form : `NormalForm`
638 Normal form being transformed to.
640 Returns
641 -------
642 normalized : `TransformationWrapper`
643 An expression equivalent to ``operator.apply(other, self)``
644 for which ``normalized.satisfies(form)`` returns `True`.
646 Notes
647 -----
648 Should only be called by `_normalizeDispatch` implementations; this
649 guarantees that ``other`` is atomic and ``self`` is normalized.
650 """
651 return operator.apply(other, self)
653 def _satisfiesDispatchBinary(
654 self,
655 outer: LogicalBinaryOperator,
656 lhs: TransformationWrapper,
657 inner: LogicalBinaryOperator,
658 rhs: TransformationWrapper,
659 *,
660 form: NormalForm,
661 ) -> bool:
662 """Test whether ``outer.apply(self, inner.apply(lhs, rhs))`` is in a
663 normal form.
665 The default implementation is appropriate for "atomic" classes that
666 never contain any AND or OR operations at all.
668 Parameters
669 ----------
670 outer : `LogicalBinaryOperator`
671 Outer operator for the expression being tested.
672 lhs : `TransformationWrapper`
673 One inner operand for the expression being tested.
674 inner : `LogicalBinaryOperator`
675 Inner operator for the expression being tested. This may or may
676 not be the same as ``outer``.
677 rhs: `TransformationWrapper`
678 The other inner operand for the expression being tested.
679 form : `NormalForm`
680 Normal form being transformed to.
682 Returns
683 -------
684 satisfies : `bool`
685 Whether ``outer.apply(self, inner.apply(lhs, rhs))`` satisfies the
686 requirements of ``form``.
688 Notes
689 -----
690 Should only be called by `_satisfiesDispatch` implementations; this
691 guarantees that ``self``, ``lhs``, and ``rhs`` are all normalized.
692 """
693 return form.allows(inner=inner, outer=outer)
695 def _normalizeDispatchBinary(
696 self,
697 outer: LogicalBinaryOperator,
698 lhs: TransformationWrapper,
699 inner: LogicalBinaryOperator,
700 rhs: TransformationWrapper,
701 *,
702 form: NormalForm,
703 ) -> TransformationWrapper:
704 """Return an expression equivalent to
705 ``outer.apply(self, inner.apply(lhs, rhs))``, meeting the guarantees of
706 `normalize`.
708 The default implementation is appropriate for "atomic" classes that
709 never contain any AND or OR operations at all.
711 Parameters
712 ----------
713 outer : `LogicalBinaryOperator`
714 Outer operator for the expression being transformed.
715 lhs : `TransformationWrapper`
716 One inner operand for the expression being transformed.
717 inner : `LogicalBinaryOperator`
718 Inner operator for the expression being transformed.
719 rhs: `TransformationWrapper`
720 The other inner operand for the expression being transformed.
721 form : `NormalForm`
722 Normal form being transformed to.
724 Returns
725 -------
726 normalized : `TransformationWrapper`
727 An expression equivalent to
728 ``outer.apply(self, inner.apply(lhs, rhs))`` for which
729 ``normalized.satisfies(form)`` returns `True`.
731 Notes
732 -----
733 Should only be called by `_normalizeDispatch` implementations; this
734 guarantees that ``self``, ``lhs``, and ``rhs`` are all normalized.
735 """
736 if form.allows(inner=inner, outer=outer):
737 return outer.apply(inner.apply(lhs, rhs), self)
738 else:
739 return inner.apply(
740 outer.apply(lhs, self).normalize(form),
741 outer.apply(rhs, self).normalize(form),
742 )
744 @abstractmethod
745 def unwrap(self) -> Node:
746 """Return an transformed expression tree.
748 Return
749 ------
750 tree : `Node`
751 Tree node representing the same expression (and form) as ``self``.
752 """
753 raise NotImplementedError()
756class Opaque(TransformationWrapper):
757 """A `TransformationWrapper` implementation for tree nodes that do not need
758 to be modified in boolean expression transformations.
760 This includes all identifiers, literals, and operators whose arguments are
761 not boolean.
763 Parameters
764 ----------
765 node : `Node`
766 Node wrapped by ``self``.
767 precedence : `PrecedenceTier`
768 Enumeration indicating how tightly this node is bound.
769 """
771 def __init__(self, node: Node, precedence: PrecedenceTier):
772 self._node = node
773 self._precedence = precedence
775 __slots__ = ("_node", "_precedence")
777 def __str__(self) -> str:
778 return str(self._node)
780 @property
781 def precedence(self) -> PrecedenceTier:
782 # Docstring inherited from `TransformationWrapper`.
783 return self._precedence
785 def not_(self) -> TransformationWrapper:
786 # Docstring inherited from `TransformationWrapper`.
787 return LogicalNot(self)
789 def unwrap(self) -> Node:
790 # Docstring inherited from `TransformationWrapper`.
791 return self._node
794class LogicalNot(TransformationWrapper):
795 """A `TransformationWrapper` implementation for logical NOT operations.
797 Parameters
798 ----------
799 operand : `TransformationWrapper`
800 Wrapper representing the operand of the NOT operation.
802 Notes
803 -----
804 Instances should always be created by calling `not_` on an existing
805 `TransformationWrapper` instead of calling `LogicalNot` directly.
806 `LogicalNot` should only be called directly by `Opaque.not_`. This
807 guarantees that double-negatives are simplified away and NOT operations
808 are moved inside any OR and AND operations at construction.
809 """
811 def __init__(self, operand: Opaque):
812 self._operand = operand
814 __slots__ = ("_operand",)
816 def __str__(self) -> str:
817 return f"not({self._operand})"
819 @property
820 def precedence(self) -> PrecedenceTier:
821 # Docstring inherited from `TransformationWrapper`.
822 return PrecedenceTier.UNARY
824 def not_(self) -> TransformationWrapper:
825 # Docstring inherited from `TransformationWrapper`.
826 return self._operand
828 def unwrap(self) -> Node:
829 # Docstring inherited from `TransformationWrapper`.
830 node = self._operand.unwrap()
831 if PrecedenceTier.needsParens(self.precedence, self._operand.precedence):
832 node = Parens(node)
833 return UnaryOp("NOT", node)
836class LogicalBinaryOperation(TransformationWrapper):
837 """A `TransformationWrapper` implementation for logical OR and NOT
838 implementations.
840 Parameters
841 ----------
842 lhs : `TransformationWrapper`
843 First operand.
844 operator : `LogicalBinaryOperator`
845 Enumeration representing the operator.
846 rhs : `TransformationWrapper`
847 Second operand.
848 """
850 def __init__(
851 self, lhs: TransformationWrapper, operator: LogicalBinaryOperator, rhs: TransformationWrapper
852 ):
853 self._lhs = lhs
854 self._operator = operator
855 self._rhs = rhs
856 self._satisfiesCache: Dict[NormalForm, bool] = {}
858 __slots__ = ("_lhs", "_operator", "_rhs", "_satisfiesCache")
860 def __str__(self) -> str:
861 return f"{self._operator.name.lower()}({self._lhs}, {self._rhs})"
863 @property
864 def precedence(self) -> PrecedenceTier:
865 # Docstring inherited from `TransformationWrapper`.
866 return BINARY_OPERATOR_PRECEDENCE[self._operator.name]
868 def not_(self) -> TransformationWrapper:
869 # Docstring inherited from `TransformationWrapper`.
870 return LogicalBinaryOperation(
871 self._lhs.not_(),
872 LogicalBinaryOperator(not self._operator.value),
873 self._rhs.not_(),
874 )
876 def satisfies(self, form: NormalForm) -> bool:
877 # Docstring inherited from `TransformationWrapper`.
878 r = self._satisfiesCache.get(form)
879 if r is None:
880 r = (
881 self._lhs.satisfies(form)
882 and self._rhs.satisfies(form)
883 and self._lhs._satisfiesDispatch(self._operator, self._rhs, form=form)
884 )
885 self._satisfiesCache[form] = r
886 return r
888 def normalize(self, form: NormalForm) -> TransformationWrapper:
889 # Docstring inherited from `TransformationWrapper`.
890 if self.satisfies(form):
891 return self
892 lhs = self._lhs.normalize(form)
893 rhs = self._rhs.normalize(form)
894 return lhs._normalizeDispatch(self._operator, rhs, form=form)
896 def flatten(self, operator: LogicalBinaryOperator) -> Iterator[TransformationWrapper]:
897 # Docstring inherited from `TransformationWrapper`.
898 if operator is self._operator:
899 yield from self._lhs.flatten(operator)
900 yield from self._rhs.flatten(operator)
901 else:
902 yield self
904 def _satisfiesDispatch(
905 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
906 ) -> bool:
907 # Docstring inherited from `TransformationWrapper`.
908 return other._satisfiesDispatchBinary(operator, self._lhs, self._operator, self._rhs, form=form)
910 def _normalizeDispatch(
911 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
912 ) -> TransformationWrapper:
913 # Docstring inherited from `TransformationWrapper`.
914 return other._normalizeDispatchBinary(operator, self._lhs, self._operator, self._rhs, form=form)
916 def _satisfiesDispatchAtomic(
917 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
918 ) -> bool:
919 # Docstring inherited from `TransformationWrapper`.
920 return form.allows(outer=operator, inner=self._operator)
922 def _normalizeDispatchAtomic(
923 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
924 ) -> TransformationWrapper:
925 # Docstring inherited from `TransformationWrapper`.
926 # Normalizes an expression of the form:
927 #
928 # operator.apply(
929 # other,
930 # self._operator.apply(self._lhs, self._rhs),
931 # )
932 #
933 if form.allows(outer=operator, inner=self._operator):
934 return operator.apply(other, self)
935 else:
936 return self._operator.apply(
937 operator.apply(other, self._lhs).normalize(form),
938 operator.apply(other, self._rhs).normalize(form),
939 )
941 def _satisfiesDispatchBinary(
942 self,
943 outer: LogicalBinaryOperator,
944 lhs: TransformationWrapper,
945 inner: LogicalBinaryOperator,
946 rhs: TransformationWrapper,
947 *,
948 form: NormalForm,
949 ) -> bool:
950 # Docstring inherited from `TransformationWrapper`.
951 return form.allows(outer=outer, inner=inner) and form.allows(outer=outer, inner=self._operator)
953 def _normalizeDispatchBinary(
954 self,
955 outer: LogicalBinaryOperator,
956 lhs: TransformationWrapper,
957 inner: LogicalBinaryOperator,
958 rhs: TransformationWrapper,
959 *,
960 form: NormalForm,
961 ) -> TransformationWrapper:
962 # Docstring inherited from `TransformationWrapper`.
963 # Normalizes an expression of the form:
964 #
965 # outer.apply(
966 # inner.apply(lhs, rhs),
967 # self._operator.apply(self._lhs, self._rhs),
968 # )
969 #
970 if form.allows(inner=inner, outer=outer):
971 other = inner.apply(lhs, rhs)
972 if form.allows(inner=self._operator, outer=outer):
973 return outer.apply(other, self)
974 else:
975 return self._operator.apply(
976 outer.apply(other, self._lhs).normalize(form),
977 outer.apply(other, self._rhs).normalize(form),
978 )
979 else:
980 if form.allows(inner=self._operator, outer=outer):
981 return inner.apply(
982 outer.apply(lhs, self).normalize(form), outer.apply(rhs, self).normalize(form)
983 )
984 else:
985 assert form.allows(inner=inner, outer=self._operator)
986 return self._operator.apply(
987 inner.apply(
988 outer.apply(lhs, self._lhs).normalize(form),
989 outer.apply(lhs, self._rhs).normalize(form),
990 ),
991 inner.apply(
992 outer.apply(rhs, self._lhs).normalize(form),
993 outer.apply(rhs, self._rhs).normalize(form),
994 ),
995 )
997 def unwrap(self) -> Node:
998 # Docstring inherited from `TransformationWrapper`.
999 lhsNode = self._lhs.unwrap()
1000 if PrecedenceTier.needsParens(self.precedence, self._lhs.precedence):
1001 lhsNode = Parens(lhsNode)
1002 rhsNode = self._rhs.unwrap()
1003 if PrecedenceTier.needsParens(self.precedence, self._rhs.precedence):
1004 rhsNode = Parens(rhsNode)
1005 return BinaryOp(lhsNode, self._operator.name, rhsNode)
1008class TransformationVisitor(TreeVisitor[TransformationWrapper]):
1009 """A `TreeVisitor` implementation that constructs a `TransformationWrapper`
1010 tree when applied to a `Node` tree.
1011 """
1013 def visitNumericLiteral(self, value: str, node: Node) -> TransformationWrapper:
1014 # Docstring inherited from TreeVisitor.visitNumericLiteral
1015 return Opaque(node, PrecedenceTier.TOKEN)
1017 def visitStringLiteral(self, value: str, node: Node) -> TransformationWrapper:
1018 # Docstring inherited from TreeVisitor.visitStringLiteral
1019 return Opaque(node, PrecedenceTier.TOKEN)
1021 def visitTimeLiteral(self, value: astropy.time.Time, node: Node) -> TransformationWrapper:
1022 # Docstring inherited from TreeVisitor.visitTimeLiteral
1023 return Opaque(node, PrecedenceTier.TOKEN)
1025 def visitRangeLiteral(
1026 self, start: int, stop: int, stride: Optional[int], node: Node
1027 ) -> TransformationWrapper:
1028 # Docstring inherited from TreeVisitor.visitRangeLiteral
1029 return Opaque(node, PrecedenceTier.TOKEN)
1031 def visitIdentifier(self, name: str, node: Node) -> TransformationWrapper:
1032 # Docstring inherited from TreeVisitor.visitIdentifier
1033 return Opaque(node, PrecedenceTier.TOKEN)
1035 def visitUnaryOp(
1036 self,
1037 operator: str,
1038 operand: TransformationWrapper,
1039 node: Node,
1040 ) -> TransformationWrapper:
1041 # Docstring inherited from TreeVisitor.visitUnaryOp
1042 if operator == "NOT":
1043 return operand.not_()
1044 else:
1045 return Opaque(node, PrecedenceTier.UNARY)
1047 def visitBinaryOp(
1048 self,
1049 operator: str,
1050 lhs: TransformationWrapper,
1051 rhs: TransformationWrapper,
1052 node: Node,
1053 ) -> TransformationWrapper:
1054 # Docstring inherited from TreeVisitor.visitBinaryOp
1055 logical = LogicalBinaryOperator.__members__.get(operator)
1056 if logical is not None:
1057 return LogicalBinaryOperation(lhs, logical, rhs)
1058 return Opaque(node, BINARY_OPERATOR_PRECEDENCE[operator])
1060 def visitIsIn(
1061 self,
1062 lhs: TransformationWrapper,
1063 values: List[TransformationWrapper],
1064 not_in: bool,
1065 node: Node,
1066 ) -> TransformationWrapper:
1067 # Docstring inherited from TreeVisitor.visitIsIn
1068 return Opaque(node, PrecedenceTier.COMPARISON)
1070 def visitParens(self, expression: TransformationWrapper, node: Node) -> TransformationWrapper:
1071 # Docstring inherited from TreeVisitor.visitParens
1072 return expression
1074 def visitTupleNode(self, items: Tuple[TransformationWrapper, ...], node: Node) -> TransformationWrapper:
1075 # Docstring inherited from TreeVisitor.visitTupleNode
1076 return Opaque(node, PrecedenceTier.TOKEN)
1078 def visitPointNode(
1079 self, ra: TransformationWrapper, dec: TransformationWrapper, node: Node
1080 ) -> TransformationWrapper:
1081 # Docstring inherited from TreeVisitor.visitPointNode
1082 raise NotImplementedError("POINT() function is not supported yet")
1085class TreeReconstructionVisitor(NormalFormVisitor[Node, Node, Node]):
1086 """A `NormalFormVisitor` that reconstructs complete expression tree.
1088 Outside code should use `NormalFormExpression.toTree` (which delegates to
1089 this visitor) instead.
1090 """
1092 def visitBranch(self, node: Node) -> Node:
1093 # Docstring inherited from NormalFormVisitor.
1094 return node
1096 def _visitSequence(self, branches: Sequence[Node], operator: LogicalBinaryOperator) -> Node:
1097 """Common recursive implementation for `visitInner` and `visitOuter`.
1099 Parameters
1100 ----------
1101 branches : `Sequence`
1102 Sequence of return values from calls to `visitBranch`, representing
1103 a visited set of operands combined in the expression by
1104 ``operator``.
1105 operator : `LogicalBinaryOperator`
1106 Operator that joins the elements of ``branches``.
1108 Returns
1109 -------
1110 result
1111 Result of the final call to ``visitBranch(node)``.
1112 node : `Node`
1113 Hierarchical expression tree equivalent to joining ``branches``
1114 with ``operator``.
1115 """
1116 first, *rest = branches
1117 if not rest:
1118 return first
1119 merged = self._visitSequence(rest, operator)
1120 node = BinaryOp(first, operator.name, merged)
1121 return self.visitBranch(node)
1123 def visitInner(self, branches: Sequence[Node], form: NormalForm) -> Node:
1124 # Docstring inherited from NormalFormVisitor.
1125 node = self._visitSequence(branches, form.inner)
1126 if len(branches) > 1:
1127 node = Parens(node)
1128 return node
1130 def visitOuter(self, branches: Sequence[Node], form: NormalForm) -> Node:
1131 # Docstring inherited from NormalFormVisitor.
1132 node = self._visitSequence(branches, form.outer)
1133 if isinstance(node, Parens):
1134 node = node.expr
1135 return node