Coverage for python/lsst/daf/butler/registry/queries/exprParser/normalForm.py : 37%

Hot-keys 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 .treeVisitor import TreeVisitor
46from .exprTree import BinaryOp, Node, Parens, UnaryOp
49class LogicalBinaryOperator(enum.Enum):
50 """Enumeration for logical binary operators.
52 These intentionally have the same names (including capitalization) as the
53 string binary operators used in the parser itself. Boolean values are
54 used to enable generic code that works on either operator (particularly
55 ``LogicalBinaryOperator(not self)`` as a way to get the other operator).
57 Which is `True` and which is `False` is just a convention, but one shared
58 by `LogicalBinaryOperator` and `NormalForm`.
59 """
61 AND = True
62 OR = False
64 def apply(self, lhs: TransformationWrapper, rhs: TransformationWrapper) -> LogicalBinaryOperation:
65 """Return a `TransformationWrapper` object representing this operator
66 applied to the given operands.
68 This is simply syntactic sugar for the `LogicalBinaryOperation`
69 constructor.
71 Parameters
72 ----------
73 lhs: `TransformationWrapper`
74 First operand.
75 rhs: `TransformationWrapper`
76 Second operand.
78 Returns
79 -------
80 operation : `LogicalBinaryOperation`
81 Object representing the operation.
82 """
83 return LogicalBinaryOperation(lhs, self, rhs)
86class NormalForm(enum.Enum):
87 """Enumeration for boolean normal forms.
89 Both normal forms require all NOT operands to be moved "inside" any AND
90 or OR operations (i.e. by applying DeMorgan's laws), with either all AND
91 operations or all OR operations appearing outside the other (`CONJUNCTIVE`,
92 and `DISJUNCTIVE`, respectively).
94 Boolean values are used here to enable generic code that works on either
95 form, and for interoperability with `LogicalBinaryOperator`.
97 Which is `True` and which is `False` is just a convention, but one shared
98 by `LogicalBinaryOperator` and `NormalForm`.
99 """
101 CONJUNCTIVE = True
102 """Form in which AND operations may have OR operands, but not the reverse.
104 For example, ``A AND (B OR C)`` is in conjunctive normal form, but
105 ``A OR (B AND C)`` and ``A AND (B OR (C AND D))`` are not.
106 """
108 DISJUNCTIVE = False
109 """Form in which OR operations may have AND operands, but not the reverse.
111 For example, ``A OR (B AND C)`` is in disjunctive normal form, but
112 ``A AND (B OR C)`` and ``A OR (B AND (C OR D))`` are not.
113 """
115 @property
116 def inner(self) -> LogicalBinaryOperator:
117 """The operator that is not permitted to contain operations of the
118 other type in this form.
120 Note that this operation may still appear as the outermost operator in
121 an expression in this form if there are no operations of the other
122 type.
123 """
124 return LogicalBinaryOperator(not self.value)
126 @property
127 def outer(self) -> LogicalBinaryOperator:
128 """The operator that is not permitted to be contained by operations of
129 the other type in this form.
131 Note that an operation of this type is not necessarily the outermost
132 operator in an expression in this form; the expression need not contain
133 any operations of this type.
134 """
135 return LogicalBinaryOperator(self.value)
137 def allows(self, *, inner: LogicalBinaryOperator, outer: LogicalBinaryOperator) -> bool:
138 """Test whether this form allows the given operator relationships.
140 Parameters
141 ----------
142 inner : `LogicalBinaryOperator`
143 Inner operator in an expression. May be the same as ``outer``.
144 outer : `LogicalBinaryOperator`
145 Outer operator in an expression. May be the same as ``inner``.
147 Returns
148 -------
149 allowed : `bool`
150 Whether this operation relationship is allowed.
151 """
152 return inner == outer or outer is self.outer
155_T = TypeVar("_T")
156_U = TypeVar("_U")
157_V = TypeVar("_V")
160class NormalFormVisitor(Generic[_T, _U, _V]):
161 """A visitor interface for `NormalFormExpression`.
163 A visitor implementation may inherit from both `NormalFormVisitor` and
164 `TreeVisitor` and implement `visitBranch` as ``node.visit(self)`` to
165 visit each node of the entire tree (or similarly with composition, etc).
166 In this case, `TreeVisitor.visitBinaryOp` will never be called with a
167 logical OR or AND operation, because these will all have been moved into
168 calls to `visitInner` and `visitOuter` instead.
170 See also `NormalFormExpression.visit`.
171 """
173 @abstractmethod
174 def visitBranch(self, node: Node) -> _T:
175 """Visit a regular `Node` and its child nodes.
177 Parameters
178 ----------
179 node : `Node`
180 A branch of the expression tree that contains no AND or OR
181 operations.
183 Returns
184 -------
185 result
186 Implementation-defined result to be gathered and passed to
187 `visitInner`.
188 """
189 raise NotImplementedError()
191 @abstractmethod
192 def visitInner(self, branches: Sequence[_T], form: NormalForm) -> _U:
193 """Visit a sequence of inner OR (for `~NormalForm.CONJUNCTIVE` form)
194 or AND (for `~NormalForm.DISJUNCTIVE`) operands.
196 Parameters
197 ----------
198 branches : `Sequence`
199 Sequence of tuples, where the first element in each tuple is the
200 result of a call to `visitBranch`, and the second is the `Node` on
201 which `visitBranch` was called.
202 form : `NormalForm`
203 Form this expression is in. ``form.inner`` is the operator that
204 joins the operands in ``branches``.
206 Returns
207 -------
208 result
209 Implementation-defined result to be gathered and passed to
210 `visitOuter`.
211 """
212 raise NotImplementedError()
214 @abstractmethod
215 def visitOuter(self, branches: Sequence[_U], form: NormalForm) -> _V:
216 """Visit the sequence of outer AND (for `~NormalForm.CONJUNCTIVE` form)
217 or OR (for `~NormalForm.DISJUNCTIVE`) operands.
219 Parameters
220 ----------
221 branches : `Sequence`
222 Sequence of return values from calls to `visitInner`.
223 form : `NormalForm`
224 Form this expression is in. ``form.outer`` is the operator that
225 joins the operands in ``branches``.
227 Returns
228 -------
229 result
230 Implementation-defined result to be returned by
231 `NormalFormExpression.visitNormalForm`.
232 """
233 raise NotImplementedError()
236class NormalFormExpression:
237 """A boolean expression in a standard normal form.
239 Most code should use `fromTree` to construct new instances instead of
240 calling the constructor directly. See `NormalForm` for a description of
241 the two forms.
243 Parameters
244 ----------
245 nodes : `Sequence` [ `Sequence` [ `Node` ] ]
246 Non-AND, non-OR branches of three tree, with the AND and OR operations
247 combining them represented by position in the nested sequence - the
248 inner sequence is combined via ``form.inner``, and the outer sequence
249 combines those via ``form.outer``.
250 form : `NormalForm`
251 Enumeration value indicating the form this expression is in.
252 """
253 def __init__(self, nodes: Sequence[Sequence[Node]], form: NormalForm):
254 self._form = form
255 self._nodes = nodes
257 def __str__(self) -> str:
258 return str(self.toTree())
260 @staticmethod
261 def fromTree(root: Node, form: NormalForm) -> NormalFormExpression:
262 """Construct a `NormalFormExpression` by normalizing an arbitrary
263 expression tree.
265 Parameters
266 ----------
267 root : `Node`
268 Root of the tree to be normalized.
269 form : `NormalForm`
270 Enumeration value indicating the form to normalize to.
272 Notes
273 -----
274 Converting an arbitrary boolean expression to either normal form is an
275 NP-hard problem, and this is a brute-force algorithm. I'm not sure
276 what its actual algorithmic scaling is, but it'd definitely be a bad
277 idea to attempt to normalize an expression with hundreds or thousands
278 of clauses.
279 """
280 wrapper = root.visit(TransformationVisitor()).normalize(form)
281 nodes = []
282 for outerOperands in wrapper.flatten(form.outer):
283 nodes.append([w.unwrap() for w in outerOperands.flatten(form.inner)])
284 return NormalFormExpression(nodes, form=form)
286 @property
287 def form(self) -> NormalForm:
288 """Enumeration value indicating the form this expression is in.
289 """
290 return self._form
292 def visit(self, visitor: NormalFormVisitor[_T, _U, _V]) -> _V:
293 """Apply a visitor to the expression, explicitly taking advantage of
294 the special structure guaranteed by the normal forms.
296 Parameters
297 ----------
298 visitor : `NormalFormVisitor`
299 Visitor object to apply.
301 Returns
302 -------
303 result
304 Return value from calling ``visitor.visitOuter`` after visiting
305 all other nodes.
306 """
307 visitedOuterBranches: List[_U] = []
308 for nodeInnerBranches in self._nodes:
309 visitedInnerBranches = [visitor.visitBranch(node) for node in nodeInnerBranches]
310 visitedOuterBranches.append(visitor.visitInner(visitedInnerBranches, self.form))
311 return visitor.visitOuter(visitedOuterBranches, self.form)
313 def toTree(self) -> Node:
314 """Convert ``self`` back into an equivalent tree, preserving the
315 form of the boolean expression but representing it in memory as
316 a tree.
318 Returns
319 -------
320 tree : `Node`
321 Root of the tree equivalent to ``self``.
322 """
323 visitor = TreeReconstructionVisitor()
324 return self.visit(visitor)
327class PrecedenceTier(enum.Enum):
328 """An enumeration used to track operator precedence.
330 This enum is currently used only to inject parentheses into boolean
331 logic that has been manipulated into a new form, and because those
332 parentheses are only used for stringification, the goal here is human
333 readability, not precision.
335 Lower enum values represent tighter binding, but code deciding whether to
336 inject parentheses should always call `needsParens` instead of comparing
337 values directly.
338 """
340 TOKEN = 0
341 """Precedence tier for literals, identifiers, and expressions already in
342 parentheses.
343 """
345 UNARY = 1
346 """Precedence tier for unary operators, which always bind more tightly
347 than any binary operator.
348 """
350 VALUE_BINARY_OP = 2
351 """Precedence tier for binary operators that return non-boolean values.
352 """
354 COMPARISON = 3
355 """Precendence tier for binary comparison operators that accept non-boolean
356 values and return boolean values.
357 """
359 AND = 4
360 """Precendence tier for logical AND.
361 """
363 OR = 5
364 """Precedence tier for logical OR.
365 """
367 @classmethod
368 def needsParens(cls, outer: PrecedenceTier, inner: PrecedenceTier) -> bool:
369 """Test whether parentheses should be added around an operand.
371 Parameters
372 ----------
373 outer : `PrecedenceTier`
374 Precedence tier for the operation.
375 inner : `PrecedenceTier`
376 Precedence tier for the operand.
378 Returns
379 -------
380 needed : `bool`
381 If `True`, parentheses should be added.
383 Notes
384 -----
385 This method special cases logical binary operators for readability,
386 adding parentheses around ANDs embedded in ORs, and avoiding them in
387 chains of the same operator. If it is ever actually used with other
388 binary operators as ``outer``, those would probably merit similar
389 attention (or a totally new approach); its current approach would
390 aggressively add a lot of unfortunate parentheses because (aside from
391 this special-casing) it doesn't know about commutativity.
393 In fact, this method is rarely actually used for logical binary
394 operators either; the `LogicalBinaryOperation.unwrap` method that calls
395 it is never invoked by `NormalFormExpression` (the main public
396 interface), because those operators are flattened out (see
397 `TransormationWrapper.flatten`) instead. Parentheses are instead added
398 there by `TreeReconstructionVisitor`, which is simpler because the
399 structure of operators is restricted.
400 """
401 if outer is cls.OR and inner is cls.AND:
402 return True
403 if outer is cls.OR and inner is cls.OR:
404 return False
405 if outer is cls.AND and inner is cls.AND:
406 return False
407 return outer.value <= inner.value
410BINARY_OPERATOR_PRECEDENCE = {
411 "=": PrecedenceTier.COMPARISON,
412 "!=": PrecedenceTier.COMPARISON,
413 "<": PrecedenceTier.COMPARISON,
414 "<=": PrecedenceTier.COMPARISON,
415 ">": PrecedenceTier.COMPARISON,
416 ">=": 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