Coverage for python/lsst/daf/butler/registry/queries/expressions/normalForm.py: 44%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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)
30from abc import ABC, abstractmethod
31import enum
32from typing import (
33 Dict,
34 Generic,
35 Iterator,
36 List,
37 Optional,
38 Sequence,
39 Tuple,
40 TypeVar,
41)
43import astropy.time
45from .parser import BinaryOp, Node, Parens, TreeVisitor, UnaryOp
48class LogicalBinaryOperator(enum.Enum):
49 """Enumeration for logical binary operators.
51 These intentionally have the same names (including capitalization) as the
52 string binary operators used in the parser itself. Boolean values are
53 used to enable generic code that works on either operator (particularly
54 ``LogicalBinaryOperator(not self)`` as a way to get the other operator).
56 Which is `True` and which is `False` is just a convention, but one shared
57 by `LogicalBinaryOperator` and `NormalForm`.
58 """
60 AND = True
61 OR = False
63 def apply(self, lhs: TransformationWrapper, rhs: TransformationWrapper) -> LogicalBinaryOperation:
64 """Return a `TransformationWrapper` object representing this operator
65 applied to the given operands.
67 This is simply syntactic sugar for the `LogicalBinaryOperation`
68 constructor.
70 Parameters
71 ----------
72 lhs: `TransformationWrapper`
73 First operand.
74 rhs: `TransformationWrapper`
75 Second operand.
77 Returns
78 -------
79 operation : `LogicalBinaryOperation`
80 Object representing the operation.
81 """
82 return LogicalBinaryOperation(lhs, self, rhs)
85class NormalForm(enum.Enum):
86 """Enumeration for boolean normal forms.
88 Both normal forms require all NOT operands to be moved "inside" any AND
89 or OR operations (i.e. by applying DeMorgan's laws), with either all AND
90 operations or all OR operations appearing outside the other (`CONJUNCTIVE`,
91 and `DISJUNCTIVE`, respectively).
93 Boolean values are used here to enable generic code that works on either
94 form, and for interoperability with `LogicalBinaryOperator`.
96 Which is `True` and which is `False` is just a convention, but one shared
97 by `LogicalBinaryOperator` and `NormalForm`.
98 """
100 CONJUNCTIVE = True
101 """Form in which AND operations may have OR operands, but not the reverse.
103 For example, ``A AND (B OR C)`` is in conjunctive normal form, but
104 ``A OR (B AND C)`` and ``A AND (B OR (C AND D))`` are not.
105 """
107 DISJUNCTIVE = False
108 """Form in which OR operations may have AND operands, but not the reverse.
110 For example, ``A OR (B AND C)`` is in disjunctive normal form, but
111 ``A AND (B OR C)`` and ``A OR (B AND (C OR D))`` are not.
112 """
114 @property
115 def inner(self) -> LogicalBinaryOperator:
116 """The operator that is not permitted to contain operations of the
117 other type in this form.
119 Note that this operation may still appear as the outermost operator in
120 an expression in this form if there are no operations of the other
121 type.
122 """
123 return LogicalBinaryOperator(not self.value)
125 @property
126 def outer(self) -> LogicalBinaryOperator:
127 """The operator that is not permitted to be contained by operations of
128 the other type in this form.
130 Note that an operation of this type is not necessarily the outermost
131 operator in an expression in this form; the expression need not contain
132 any operations of this type.
133 """
134 return LogicalBinaryOperator(self.value)
136 def allows(self, *, inner: LogicalBinaryOperator, outer: LogicalBinaryOperator) -> bool:
137 """Test whether this form allows the given operator relationships.
139 Parameters
140 ----------
141 inner : `LogicalBinaryOperator`
142 Inner operator in an expression. May be the same as ``outer``.
143 outer : `LogicalBinaryOperator`
144 Outer operator in an expression. May be the same as ``inner``.
146 Returns
147 -------
148 allowed : `bool`
149 Whether this operation relationship is allowed.
150 """
151 return inner == outer or outer is self.outer
154_T = TypeVar("_T")
155_U = TypeVar("_U")
156_V = TypeVar("_V")
159class NormalFormVisitor(Generic[_T, _U, _V]):
160 """A visitor interface for `NormalFormExpression`.
162 A visitor implementation may inherit from both `NormalFormVisitor` and
163 `TreeVisitor` and implement `visitBranch` as ``node.visit(self)`` to
164 visit each node of the entire tree (or similarly with composition, etc).
165 In this case, `TreeVisitor.visitBinaryOp` will never be called with a
166 logical OR or AND operation, because these will all have been moved into
167 calls to `visitInner` and `visitOuter` instead.
169 See also `NormalFormExpression.visit`.
170 """
172 @abstractmethod
173 def visitBranch(self, node: Node) -> _T:
174 """Visit a regular `Node` and its child nodes.
176 Parameters
177 ----------
178 node : `Node`
179 A branch of the expression tree that contains no AND or OR
180 operations.
182 Returns
183 -------
184 result
185 Implementation-defined result to be gathered and passed to
186 `visitInner`.
187 """
188 raise NotImplementedError()
190 @abstractmethod
191 def visitInner(self, branches: Sequence[_T], form: NormalForm) -> _U:
192 """Visit a sequence of inner OR (for `~NormalForm.CONJUNCTIVE` form)
193 or AND (for `~NormalForm.DISJUNCTIVE`) operands.
195 Parameters
196 ----------
197 branches : `Sequence`
198 Sequence of tuples, where the first element in each tuple is the
199 result of a call to `visitBranch`, and the second is the `Node` on
200 which `visitBranch` was called.
201 form : `NormalForm`
202 Form this expression is in. ``form.inner`` is the operator that
203 joins the operands in ``branches``.
205 Returns
206 -------
207 result
208 Implementation-defined result to be gathered and passed to
209 `visitOuter`.
210 """
211 raise NotImplementedError()
213 @abstractmethod
214 def visitOuter(self, branches: Sequence[_U], form: NormalForm) -> _V:
215 """Visit the sequence of outer AND (for `~NormalForm.CONJUNCTIVE` form)
216 or OR (for `~NormalForm.DISJUNCTIVE`) operands.
218 Parameters
219 ----------
220 branches : `Sequence`
221 Sequence of return values from calls to `visitInner`.
222 form : `NormalForm`
223 Form this expression is in. ``form.outer`` is the operator that
224 joins the operands in ``branches``.
226 Returns
227 -------
228 result
229 Implementation-defined result to be returned by
230 `NormalFormExpression.visitNormalForm`.
231 """
232 raise NotImplementedError()
235class NormalFormExpression:
236 """A boolean expression in a standard normal form.
238 Most code should use `fromTree` to construct new instances instead of
239 calling the constructor directly. See `NormalForm` for a description of
240 the two forms.
242 Parameters
243 ----------
244 nodes : `Sequence` [ `Sequence` [ `Node` ] ]
245 Non-AND, non-OR branches of three tree, with the AND and OR operations
246 combining them represented by position in the nested sequence - the
247 inner sequence is combined via ``form.inner``, and the outer sequence
248 combines those via ``form.outer``.
249 form : `NormalForm`
250 Enumeration value indicating the form this expression is in.
251 """
252 def __init__(self, nodes: Sequence[Sequence[Node]], form: NormalForm):
253 self._form = form
254 self._nodes = nodes
256 def __str__(self) -> str:
257 return str(self.toTree())
259 @staticmethod
260 def fromTree(root: Node, form: NormalForm) -> NormalFormExpression:
261 """Construct a `NormalFormExpression` by normalizing an arbitrary
262 expression tree.
264 Parameters
265 ----------
266 root : `Node`
267 Root of the tree to be normalized.
268 form : `NormalForm`
269 Enumeration value indicating the form to normalize to.
271 Notes
272 -----
273 Converting an arbitrary boolean expression to either normal form is an
274 NP-hard problem, and this is a brute-force algorithm. I'm not sure
275 what its actual algorithmic scaling is, but it'd definitely be a bad
276 idea to attempt to normalize an expression with hundreds or thousands
277 of clauses.
278 """
279 wrapper = root.visit(TransformationVisitor()).normalize(form)
280 nodes = []
281 for outerOperands in wrapper.flatten(form.outer):
282 nodes.append([w.unwrap() for w in outerOperands.flatten(form.inner)])
283 return NormalFormExpression(nodes, form=form)
285 @property
286 def form(self) -> NormalForm:
287 """Enumeration value indicating the form this expression is in.
288 """
289 return self._form
291 def visit(self, visitor: NormalFormVisitor[_T, _U, _V]) -> _V:
292 """Apply a visitor to the expression, explicitly taking advantage of
293 the special structure guaranteed by the normal forms.
295 Parameters
296 ----------
297 visitor : `NormalFormVisitor`
298 Visitor object to apply.
300 Returns
301 -------
302 result
303 Return value from calling ``visitor.visitOuter`` after visiting
304 all other nodes.
305 """
306 visitedOuterBranches: List[_U] = []
307 for nodeInnerBranches in self._nodes:
308 visitedInnerBranches = [visitor.visitBranch(node) for node in nodeInnerBranches]
309 visitedOuterBranches.append(visitor.visitInner(visitedInnerBranches, self.form))
310 return visitor.visitOuter(visitedOuterBranches, self.form)
312 def toTree(self) -> Node:
313 """Convert ``self`` back into an equivalent tree, preserving the
314 form of the boolean expression but representing it in memory as
315 a tree.
317 Returns
318 -------
319 tree : `Node`
320 Root of the tree equivalent to ``self``.
321 """
322 visitor = TreeReconstructionVisitor()
323 return self.visit(visitor)
326class PrecedenceTier(enum.Enum):
327 """An enumeration used to track operator precedence.
329 This enum is currently used only to inject parentheses into boolean
330 logic that has been manipulated into a new form, and because those
331 parentheses are only used for stringification, the goal here is human
332 readability, not precision.
334 Lower enum values represent tighter binding, but code deciding whether to
335 inject parentheses should always call `needsParens` instead of comparing
336 values directly.
337 """
339 TOKEN = 0
340 """Precedence tier for literals, identifiers, and expressions already in
341 parentheses.
342 """
344 UNARY = 1
345 """Precedence tier for unary operators, which always bind more tightly
346 than any binary operator.
347 """
349 VALUE_BINARY_OP = 2
350 """Precedence tier for binary operators that return non-boolean values.
351 """
353 COMPARISON = 3
354 """Precendence tier for binary comparison operators that accept non-boolean
355 values and return boolean values.
356 """
358 AND = 4
359 """Precendence tier for logical AND.
360 """
362 OR = 5
363 """Precedence tier for logical OR.
364 """
366 @classmethod
367 def needsParens(cls, outer: PrecedenceTier, inner: PrecedenceTier) -> bool:
368 """Test whether parentheses should be added around an operand.
370 Parameters
371 ----------
372 outer : `PrecedenceTier`
373 Precedence tier for the operation.
374 inner : `PrecedenceTier`
375 Precedence tier for the operand.
377 Returns
378 -------
379 needed : `bool`
380 If `True`, parentheses should be added.
382 Notes
383 -----
384 This method special cases logical binary operators for readability,
385 adding parentheses around ANDs embedded in ORs, and avoiding them in
386 chains of the same operator. If it is ever actually used with other
387 binary operators as ``outer``, those would probably merit similar
388 attention (or a totally new approach); its current approach would
389 aggressively add a lot of unfortunate parentheses because (aside from
390 this special-casing) it doesn't know about commutativity.
392 In fact, this method is rarely actually used for logical binary
393 operators either; the `LogicalBinaryOperation.unwrap` method that calls
394 it is never invoked by `NormalFormExpression` (the main public
395 interface), because those operators are flattened out (see
396 `TransormationWrapper.flatten`) instead. Parentheses are instead added
397 there by `TreeReconstructionVisitor`, which is simpler because the
398 structure of operators is restricted.
399 """
400 if outer is cls.OR and inner is cls.AND:
401 return True
402 if outer is cls.OR and inner is cls.OR:
403 return False
404 if outer is cls.AND and inner is cls.AND:
405 return False
406 return outer.value <= inner.value
409BINARY_OPERATOR_PRECEDENCE = {
410 "=": PrecedenceTier.COMPARISON,
411 "!=": PrecedenceTier.COMPARISON,
412 "<": PrecedenceTier.COMPARISON,
413 "<=": PrecedenceTier.COMPARISON,
414 ">": PrecedenceTier.COMPARISON,
415 ">=": PrecedenceTier.COMPARISON,
416 "OVERLAPS": PrecedenceTier.COMPARISON,
417 "+": PrecedenceTier.VALUE_BINARY_OP,
418 "-": PrecedenceTier.VALUE_BINARY_OP,
419 "*": PrecedenceTier.VALUE_BINARY_OP,
420 "/": PrecedenceTier.VALUE_BINARY_OP,
421 "AND": PrecedenceTier.AND,
422 "OR": PrecedenceTier.OR,
423}
426class TransformationWrapper(ABC):
427 """A base class for `Node` wrappers that can be used to transform boolean
428 operator expressions.
430 Notes
431 -----
432 `TransformationWrapper` instances should only be directly constructed by
433 each other or `TransformationVisitor`. No new subclasses beyond those in
434 this module should be added.
436 While `TransformationWrapper` and its subclasses contain the machinery
437 for transforming expressions to normal forms, it does not provide a
438 convenient interface for working with expressions in those forms; most code
439 should just use `NormalFormExpression` (which delegates to
440 `TransformationWrapper` internally) instead.
441 """
443 __slots__ = ()
445 @abstractmethod
446 def __str__(self) -> str:
447 raise NotImplementedError()
449 @property
450 @abstractmethod
451 def precedence(self) -> PrecedenceTier:
452 """Return the precedence tier for this node (`PrecedenceTier`).
454 Notes
455 -----
456 This is only used when reconstructing a full `Node` tree in `unwrap`
457 implementations, and only to inject parenthesis that are necessary for
458 correct stringification but nothing else (because the tree structure
459 itself embeds the correct order of operations already).
460 """
461 raise NotImplementedError()
463 @abstractmethod
464 def not_(self) -> TransformationWrapper:
465 """Return a wrapper that represents the logical NOT of ``self``.
467 Returns
468 -------
469 wrapper : `TransformationWrapper`
470 A wrapper that represents the logical NOT of ``self``.
471 """
472 raise NotImplementedError()
474 def satisfies(self, form: NormalForm) -> bool:
475 """Test whether this expressions is already in a normal form.
477 The default implementation is appropriate for "atomic" classes that
478 never contain any AND or OR operations at all.
480 Parameters
481 ----------
482 form : `NormalForm`
483 Enumeration indicating the form to test for.
485 Returns
486 -------
487 satisfies : `bool`
488 Whether ``self`` satisfies the requirements of ``form``.
489 """
490 return True
492 def normalize(self, form: NormalForm) -> TransformationWrapper:
493 """Return an expression equivalent to ``self`` in a normal form.
495 The default implementation is appropriate for "atomic" classes that
496 never contain any AND or OR operations at all.
498 Parameters
499 ----------
500 form : `NormalForm`
501 Enumeration indicating the form to convert to.
503 Returns
504 -------
505 normalized : `TransformationWrapper`
506 An expression equivalent to ``self`` for which
507 ``normalized.satisfies(form)`` returns `True`.
509 Notes
510 -----
511 Converting an arbitrary boolean expression to either normal form is an
512 NP-hard problem, and this is a brute-force algorithm. I'm not sure
513 what its actual algorithmic scaling is, but it'd definitely be a bad
514 idea to attempt to normalize an expression with hundreds or thousands
515 of clauses.
516 """
517 return self
519 def flatten(self, operator: LogicalBinaryOperator) -> Iterator[TransformationWrapper]:
520 """Recursively flatten the operands of any nested operators of the
521 given type.
523 For an expression like ``(A AND ((B OR C) AND D)`` (with
524 ``operator == AND``), `flatten` yields ``A, (B OR C), D``.
526 The default implementation is appropriate for "atomic" classes that
527 never contain any AND or OR operations at all.
529 Parameters
530 ----------
531 operator : `LogicalBinaryOperator`
532 Operator whose operands to flatten.
534 Returns
535 -------
536 operands : `Iterator` [ `TransformationWrapper` ]
537 Operands that, if combined with ``operator``, yield an expression
538 equivalent to ``self``.
539 """
540 yield self
542 def _satisfiesDispatch(
543 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
544 ) -> bool:
545 """Test whether ``operator.apply(self, other)`` is in a normal form.
547 The default implementation is appropriate for classes that never
548 contain any AND or OR operations at all.
550 Parameters
551 ----------
552 operator : `LogicalBinaryOperator`
553 Operator for the operation being tested.
554 other : `TransformationWrapper`
555 Other operand for the operation being tested.
556 form : `NormalForm`
557 Normal form being tested for.
559 Returns
560 -------
561 satisfies : `bool`
562 Whether ``operator.apply(self, other)`` satisfies the requirements
563 of ``form``.
565 Notes
566 -----
567 Caller guarantees that ``self`` and ``other`` are already normalized.
568 """
569 return other._satisfiesDispatchAtomic(operator, self, form=form)
571 def _normalizeDispatch(
572 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
573 ) -> TransformationWrapper:
574 """Return an expression equivalent to ``operator.apply(self, other)``
575 in a normal form.
577 The default implementation is appropriate for classes that never
578 contain any AND or OR operations at all.
580 Parameters
581 ----------
582 operator : `LogicalBinaryOperator`
583 Operator for the operation being transformed.
584 other : `TransformationWrapper`
585 Other operand for the operation being transformed.
586 form : `NormalForm`
587 Normal form being transformed to.
589 Returns
590 -------
591 normalized : `TransformationWrapper`
592 An expression equivalent to ``operator.apply(self, other)``
593 for which ``normalized.satisfies(form)`` returns `True`.
595 Notes
596 -----
597 Caller guarantees that ``self`` and ``other`` are already normalized.
598 """
599 return other._normalizeDispatchAtomic(operator, self, form=form)
601 def _satisfiesDispatchAtomic(
602 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
603 ) -> bool:
604 """Test whather ``operator.apply(other, self)`` is in a normal form.
606 The default implementation is appropriate for "atomic" classes that
607 never contain any AND or OR operations at all.
609 Parameters
610 ----------
611 operator : `LogicalBinaryOperator`
612 Operator for the operation being tested.
613 other : `TransformationWrapper`
614 Other operand for the operation being tested.
615 form : `NormalForm`
616 Normal form being tested for.
618 Returns
619 -------
620 satisfies : `bool`
621 Whether ``operator.apply(other, self)`` satisfies the requirements
622 of ``form``.
624 Notes
625 -----
626 Should only be called by `_satisfiesDispatch` implementations; this
627 guarantees that ``other`` is atomic and ``self`` is normalized.
628 """
629 return True
631 def _normalizeDispatchAtomic(
632 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
633 ) -> TransformationWrapper:
634 """Return an expression equivalent to ``operator.apply(other, self)``,
635 in a normal form.
637 The default implementation is appropriate for "atomic" classes that
638 never contain any AND or OR operations at all.
640 Parameters
641 ----------
642 operator : `LogicalBinaryOperator`
643 Operator for the operation being transformed.
644 other : `TransformationWrapper`
645 Other operand for the operation being transformed.
646 form : `NormalForm`
647 Normal form being transformed to.
649 Returns
650 -------
651 normalized : `TransformationWrapper`
652 An expression equivalent to ``operator.apply(other, self)``
653 for which ``normalized.satisfies(form)`` returns `True`.
655 Notes
656 -----
657 Should only be called by `_normalizeDispatch` implementations; this
658 guarantees that ``other`` is atomic and ``self`` is normalized.
659 """
660 return operator.apply(other, self)
662 def _satisfiesDispatchBinary(
663 self,
664 outer: LogicalBinaryOperator,
665 lhs: TransformationWrapper,
666 inner: LogicalBinaryOperator,
667 rhs: TransformationWrapper,
668 *,
669 form: NormalForm,
670 ) -> bool:
671 """Test whether ``outer.apply(self, inner.apply(lhs, rhs))`` is in a
672 normal form.
674 The default implementation is appropriate for "atomic" classes that
675 never contain any AND or OR operations at all.
677 Parameters
678 ----------
679 outer : `LogicalBinaryOperator`
680 Outer operator for the expression being tested.
681 lhs : `TransformationWrapper`
682 One inner operand for the expression being tested.
683 inner : `LogicalBinaryOperator`
684 Inner operator for the expression being tested. This may or may
685 not be the same as ``outer``.
686 rhs: `TransformationWrapper`
687 The other inner operand for the expression being tested.
688 form : `NormalForm`
689 Normal form being transformed to.
691 Returns
692 -------
693 satisfies : `bool`
694 Whether ``outer.apply(self, inner.apply(lhs, rhs))`` satisfies the
695 requirements of ``form``.
697 Notes
698 -----
699 Should only be called by `_satisfiesDispatch` implementations; this
700 guarantees that ``self``, ``lhs``, and ``rhs`` are all normalized.
701 """
702 return form.allows(inner=inner, outer=outer)
704 def _normalizeDispatchBinary(
705 self,
706 outer: LogicalBinaryOperator,
707 lhs: TransformationWrapper,
708 inner: LogicalBinaryOperator,
709 rhs: TransformationWrapper,
710 *,
711 form: NormalForm,
712 ) -> TransformationWrapper:
713 """Return an expression equivalent to
714 ``outer.apply(self, inner.apply(lhs, rhs))``, meeting the guarantees of
715 `normalize`.
717 The default implementation is appropriate for "atomic" classes that
718 never contain any AND or OR operations at all.
720 Parameters
721 ----------
722 outer : `LogicalBinaryOperator`
723 Outer operator for the expression being transformed.
724 lhs : `TransformationWrapper`
725 One inner operand for the expression being transformed.
726 inner : `LogicalBinaryOperator`
727 Inner operator for the expression being transformed.
728 rhs: `TransformationWrapper`
729 The other inner operand for the expression being transformed.
730 form : `NormalForm`
731 Normal form being transformed to.
733 Returns
734 -------
735 normalized : `TransformationWrapper`
736 An expression equivalent to
737 ``outer.apply(self, inner.apply(lhs, rhs))`` for which
738 ``normalized.satisfies(form)`` returns `True`.
740 Notes
741 -----
742 Should only be called by `_normalizeDispatch` implementations; this
743 guarantees that ``self``, ``lhs``, and ``rhs`` are all normalized.
744 """
745 if form.allows(inner=inner, outer=outer):
746 return outer.apply(inner.apply(lhs, rhs), self)
747 else:
748 return inner.apply(
749 outer.apply(lhs, self).normalize(form),
750 outer.apply(rhs, self).normalize(form),
751 )
753 @abstractmethod
754 def unwrap(self) -> Node:
755 """Return an transformed expression tree.
757 Return
758 ------
759 tree : `Node`
760 Tree node representing the same expression (and form) as ``self``.
761 """
762 raise NotImplementedError()
765class Opaque(TransformationWrapper):
766 """A `TransformationWrapper` implementation for tree nodes that do not need
767 to be modified in boolean expression transformations.
769 This includes all identifers, literals, and operators whose arguments are
770 not boolean.
772 Parameters
773 ----------
774 node : `Node`
775 Node wrapped by ``self``.
776 precedence : `PrecedenceTier`
777 Enumeration indicating how tightly this node is bound.
778 """
779 def __init__(self, node: Node, precedence: PrecedenceTier):
780 self._node = node
781 self._precedence = precedence
783 __slots__ = ("_node", "_precedence")
785 def __str__(self) -> str:
786 return str(self._node)
788 @property
789 def precedence(self) -> PrecedenceTier:
790 # Docstring inherited from `TransformationWrapper`.
791 return self._precedence
793 def not_(self) -> TransformationWrapper:
794 # Docstring inherited from `TransformationWrapper`.
795 return LogicalNot(self)
797 def unwrap(self) -> Node:
798 # Docstring inherited from `TransformationWrapper`.
799 return self._node
802class LogicalNot(TransformationWrapper):
803 """A `TransformationWrapper` implementation for logical NOT operations.
805 Parameters
806 ----------
807 operand : `TransformationWrapper`
808 Wrapper representing the operand of the NOT operation.
810 Notes
811 -----
812 Instances should aways be created by calling `not_` on an existing
813 `TransformationWrapper` instead of calling `LogicalNot` directly.
814 `LogicalNot` should only be called directly by `Opaque.not_`. This
815 guarantees that double-negatives are simplified away and NOT operations
816 are moved inside any OR and AND operations at construction.
817 """
818 def __init__(self, operand: Opaque):
819 self._operand = operand
821 __slots__ = ("_operand",)
823 def __str__(self) -> str:
824 return f"not({self._operand})"
826 @property
827 def precedence(self) -> PrecedenceTier:
828 # Docstring inherited from `TransformationWrapper`.
829 return PrecedenceTier.UNARY
831 def not_(self) -> TransformationWrapper:
832 # Docstring inherited from `TransformationWrapper`.
833 return self._operand
835 def unwrap(self) -> Node:
836 # Docstring inherited from `TransformationWrapper`.
837 node = self._operand.unwrap()
838 if PrecedenceTier.needsParens(self.precedence, self._operand.precedence):
839 node = Parens(node)
840 return UnaryOp("NOT", node)
843class LogicalBinaryOperation(TransformationWrapper):
844 """A `TransformationWrapper` implementation for logical OR and NOT
845 implementations.
847 Parameters
848 ----------
849 lhs : `TransformationWrapper`
850 First operand.
851 operator : `LogicalBinaryOperator`
852 Enumeration representing the operator.
853 rhs : `TransformationWrapper`
854 Second operand.
855 """
856 def __init__(
857 self, lhs: TransformationWrapper, operator: LogicalBinaryOperator, rhs: TransformationWrapper
858 ):
859 self._lhs = lhs
860 self._operator = operator
861 self._rhs = rhs
862 self._satisfiesCache: Dict[NormalForm, bool] = {}
864 __slots__ = ("_lhs", "_operator", "_rhs", "_satisfiesCache")
866 def __str__(self) -> str:
867 return f"{self._operator.name.lower()}({self._lhs}, {self._rhs})"
869 @property
870 def precedence(self) -> PrecedenceTier:
871 # Docstring inherited from `TransformationWrapper`.
872 return BINARY_OPERATOR_PRECEDENCE[self._operator.name]
874 def not_(self) -> TransformationWrapper:
875 # Docstring inherited from `TransformationWrapper`.
876 return LogicalBinaryOperation(
877 self._lhs.not_(),
878 LogicalBinaryOperator(not self._operator.value),
879 self._rhs.not_(),
880 )
882 def satisfies(self, form: NormalForm) -> bool:
883 # Docstring inherited from `TransformationWrapper`.
884 r = self._satisfiesCache.get(form)
885 if r is None:
886 r = (
887 self._lhs.satisfies(form)
888 and self._rhs.satisfies(form)
889 and self._lhs._satisfiesDispatch(self._operator, self._rhs, form=form)
890 )
891 self._satisfiesCache[form] = r
892 return r
894 def normalize(self, form: NormalForm) -> TransformationWrapper:
895 # Docstring inherited from `TransformationWrapper`.
896 if self.satisfies(form):
897 return self
898 lhs = self._lhs.normalize(form)
899 rhs = self._rhs.normalize(form)
900 return lhs._normalizeDispatch(self._operator, rhs, form=form)
902 def flatten(self, operator: LogicalBinaryOperator) -> Iterator[TransformationWrapper]:
903 # Docstring inherited from `TransformationWrapper`.
904 if operator is self._operator:
905 yield from self._lhs.flatten(operator)
906 yield from self._rhs.flatten(operator)
907 else:
908 yield self
910 def _satisfiesDispatch(
911 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
912 ) -> bool:
913 # Docstring inherited from `TransformationWrapper`.
914 return other._satisfiesDispatchBinary(operator, self._lhs, self._operator, self._rhs, form=form)
916 def _normalizeDispatch(
917 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
918 ) -> TransformationWrapper:
919 # Docstring inherited from `TransformationWrapper`.
920 return other._normalizeDispatchBinary(operator, self._lhs, self._operator, self._rhs, form=form)
922 def _satisfiesDispatchAtomic(
923 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
924 ) -> bool:
925 # Docstring inherited from `TransformationWrapper`.
926 return form.allows(outer=operator, inner=self._operator)
928 def _normalizeDispatchAtomic(
929 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
930 ) -> TransformationWrapper:
931 # Docstring inherited from `TransformationWrapper`.
932 # Normalizes an expression of the form:
933 #
934 # operator.apply(
935 # other,
936 # self._operator.apply(self._lhs, self._rhs),
937 # )
938 #
939 if form.allows(outer=operator, inner=self._operator):
940 return operator.apply(other, self)
941 else:
942 return self._operator.apply(
943 operator.apply(other, self._lhs).normalize(form),
944 operator.apply(other, self._rhs).normalize(form),
945 )
947 def _satisfiesDispatchBinary(
948 self,
949 outer: LogicalBinaryOperator,
950 lhs: TransformationWrapper,
951 inner: LogicalBinaryOperator,
952 rhs: TransformationWrapper,
953 *,
954 form: NormalForm,
955 ) -> bool:
956 # Docstring inherited from `TransformationWrapper`.
957 return form.allows(outer=outer, inner=inner) and form.allows(outer=outer, inner=self._operator)
959 def _normalizeDispatchBinary(
960 self,
961 outer: LogicalBinaryOperator,
962 lhs: TransformationWrapper,
963 inner: LogicalBinaryOperator,
964 rhs: TransformationWrapper,
965 *,
966 form: NormalForm,
967 ) -> TransformationWrapper:
968 # Docstring inherited from `TransformationWrapper`.
969 # Normalizes an expression of the form:
970 #
971 # outer.apply(
972 # inner.apply(lhs, rhs),
973 # self._operator.apply(self._lhs, self._rhs),
974 # )
975 #
976 if form.allows(inner=inner, outer=outer):
977 other = inner.apply(lhs, rhs)
978 if form.allows(inner=self._operator, outer=outer):
979 return outer.apply(other, self)
980 else:
981 return self._operator.apply(
982 outer.apply(other, self._lhs).normalize(form),
983 outer.apply(other, self._rhs).normalize(form),
984 )
985 else:
986 if form.allows(inner=self._operator, outer=outer):
987 return inner.apply(
988 outer.apply(lhs, self).normalize(form), outer.apply(rhs, self).normalize(form)
989 )
990 else:
991 assert form.allows(inner=inner, outer=self._operator)
992 return self._operator.apply(
993 inner.apply(
994 outer.apply(lhs, self._lhs).normalize(form),
995 outer.apply(lhs, self._rhs).normalize(form),
996 ),
997 inner.apply(
998 outer.apply(rhs, self._lhs).normalize(form),
999 outer.apply(rhs, self._rhs).normalize(form),
1000 ),
1001 )
1003 def unwrap(self) -> Node:
1004 # Docstring inherited from `TransformationWrapper`.
1005 lhsNode = self._lhs.unwrap()
1006 if PrecedenceTier.needsParens(self.precedence, self._lhs.precedence):
1007 lhsNode = Parens(lhsNode)
1008 rhsNode = self._rhs.unwrap()
1009 if PrecedenceTier.needsParens(self.precedence, self._rhs.precedence):
1010 rhsNode = Parens(rhsNode)
1011 return BinaryOp(lhsNode, self._operator.name, rhsNode)
1014class TransformationVisitor(TreeVisitor[TransformationWrapper]):
1015 """A `TreeVisitor` implementation that constructs a `TransformationWrapper`
1016 tree when applied to a `Node` tree.
1017 """
1018 def visitNumericLiteral(self, value: str, node: Node) -> TransformationWrapper:
1019 # Docstring inherited from TreeVisitor.visitNumericLiteral
1020 return Opaque(node, PrecedenceTier.TOKEN)
1022 def visitStringLiteral(self, value: str, node: Node) -> TransformationWrapper:
1023 # Docstring inherited from TreeVisitor.visitStringLiteral
1024 return Opaque(node, PrecedenceTier.TOKEN)
1026 def visitTimeLiteral(self, value: astropy.time.Time, node: Node) -> TransformationWrapper:
1027 # Docstring inherited from TreeVisitor.visitTimeLiteral
1028 return Opaque(node, PrecedenceTier.TOKEN)
1030 def visitRangeLiteral(
1031 self, start: int, stop: int, stride: Optional[int], node: Node
1032 ) -> TransformationWrapper:
1033 # Docstring inherited from TreeVisitor.visitRangeLiteral
1034 return Opaque(node, PrecedenceTier.TOKEN)
1036 def visitIdentifier(self, name: str, node: Node) -> TransformationWrapper:
1037 # Docstring inherited from TreeVisitor.visitIdentifier
1038 return Opaque(node, PrecedenceTier.TOKEN)
1040 def visitUnaryOp(
1041 self,
1042 operator: str,
1043 operand: TransformationWrapper,
1044 node: Node,
1045 ) -> TransformationWrapper:
1046 # Docstring inherited from TreeVisitor.visitUnaryOp
1047 if operator == "NOT":
1048 return operand.not_()
1049 else:
1050 return Opaque(node, PrecedenceTier.UNARY)
1052 def visitBinaryOp(
1053 self,
1054 operator: str,
1055 lhs: TransformationWrapper,
1056 rhs: TransformationWrapper,
1057 node: Node,
1058 ) -> TransformationWrapper:
1059 # Docstring inherited from TreeVisitor.visitBinaryOp
1060 logical = LogicalBinaryOperator.__members__.get(operator)
1061 if logical is not None:
1062 return LogicalBinaryOperation(lhs, logical, rhs)
1063 return Opaque(node, BINARY_OPERATOR_PRECEDENCE[operator])
1065 def visitIsIn(
1066 self,
1067 lhs: TransformationWrapper,
1068 values: List[TransformationWrapper],
1069 not_in: bool,
1070 node: Node,
1071 ) -> TransformationWrapper:
1072 # Docstring inherited from TreeVisitor.visitIsIn
1073 return Opaque(node, PrecedenceTier.COMPARISON)
1075 def visitParens(self, expression: TransformationWrapper, node: Node) -> TransformationWrapper:
1076 # Docstring inherited from TreeVisitor.visitParens
1077 return expression
1079 def visitTupleNode(self, items: Tuple[TransformationWrapper, ...], node: Node
1080 ) -> TransformationWrapper:
1081 # Docstring inherited from TreeVisitor.visitTupleNode
1082 return Opaque(node, PrecedenceTier.TOKEN)
1084 def visitPointNode(self, ra: TransformationWrapper, dec: TransformationWrapper, node: Node
1085 ) -> TransformationWrapper:
1086 # Docstring inherited from TreeVisitor.visitPointNode
1087 raise NotImplementedError("POINT() function is not supported yet")
1090class TreeReconstructionVisitor(NormalFormVisitor[Node, Node, Node]):
1091 """A `NormalFormVisitor` that reconstructs complete expression tree.
1093 Outside code should use `NormalFormExpression.toTree` (which delegates to
1094 this visitor) instead.
1095 """
1096 def visitBranch(self, node: Node) -> Node:
1097 # Docstring inherited from NormalFormVisitor.
1098 return node
1100 def _visitSequence(
1101 self, branches: Sequence[Node], operator: LogicalBinaryOperator
1102 ) -> Node:
1103 """Common recursive implementation for `visitInner` and `visitOuter`.
1105 Parameters
1106 ----------
1107 branches : `Sequence`
1108 Sequence of return values from calls to `visitBranch`, representing
1109 a visited set of operands combined in the expression by
1110 ``operator``.
1111 operator : `LogicalBinaryOperator`
1112 Operator that joins the elements of ``branches``.
1114 Returns
1115 -------
1116 result
1117 Result of the final call to ``visitBranch(node)``.
1118 node : `Node`
1119 Hierarchical expression tree equivalent to joining ``branches``
1120 with ``operator``.
1121 """
1122 first, *rest = branches
1123 if not rest:
1124 return first
1125 merged = self._visitSequence(rest, operator)
1126 node = BinaryOp(first, operator.name, merged)
1127 return self.visitBranch(node)
1129 def visitInner(self, branches: Sequence[Node], form: NormalForm) -> Node:
1130 # Docstring inherited from NormalFormVisitor.
1131 node = self._visitSequence(branches, form.inner)
1132 if len(branches) > 1:
1133 node = Parens(node)
1134 return node
1136 def visitOuter(self, branches: Sequence[Node], form: NormalForm) -> Node:
1137 # Docstring inherited from NormalFormVisitor.
1138 node = self._visitSequence(branches, form.outer)
1139 if isinstance(node, Parens):
1140 node = node.expr
1141 return node