Coverage for python/lsst/daf/butler/registry/queries/expressions/normalForm.py: 48%
253 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-25 15:14 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-25 15:14 +0000
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 collections.abc import Iterator, Sequence
33from typing import Generic, TypeVar
35import astropy.time
37from .parser import BinaryOp, Node, Parens, TreeVisitor, UnaryOp
40class LogicalBinaryOperator(enum.Enum):
41 """Enumeration for logical binary operators.
43 These intentionally have the same names (including capitalization) as the
44 string binary operators used in the parser itself. Boolean values are
45 used to enable generic code that works on either operator (particularly
46 ``LogicalBinaryOperator(not self)`` as a way to get the other operator).
48 Which is `True` and which is `False` is just a convention, but one shared
49 by `LogicalBinaryOperator` and `NormalForm`.
50 """
52 AND = True
53 OR = False
55 def apply(self, lhs: TransformationWrapper, rhs: TransformationWrapper) -> LogicalBinaryOperation:
56 """Return a `TransformationWrapper` object representing this operator
57 applied to the given operands.
59 This is simply syntactic sugar for the `LogicalBinaryOperation`
60 constructor.
62 Parameters
63 ----------
64 lhs: `TransformationWrapper`
65 First operand.
66 rhs: `TransformationWrapper`
67 Second operand.
69 Returns
70 -------
71 operation : `LogicalBinaryOperation`
72 Object representing the operation.
73 """
74 return LogicalBinaryOperation(lhs, self, rhs)
77class NormalForm(enum.Enum):
78 """Enumeration for boolean normal forms.
80 Both normal forms require all NOT operands to be moved "inside" any AND
81 or OR operations (i.e. by applying DeMorgan's laws), with either all AND
82 operations or all OR operations appearing outside the other (`CONJUNCTIVE`,
83 and `DISJUNCTIVE`, respectively).
85 Boolean values are used here to enable generic code that works on either
86 form, and for interoperability with `LogicalBinaryOperator`.
88 Which is `True` and which is `False` is just a convention, but one shared
89 by `LogicalBinaryOperator` and `NormalForm`.
90 """
92 CONJUNCTIVE = True
93 """Form in which AND operations may have OR operands, but not the reverse.
95 For example, ``A AND (B OR C)`` is in conjunctive normal form, but
96 ``A OR (B AND C)`` and ``A AND (B OR (C AND D))`` are not.
97 """
99 DISJUNCTIVE = False
100 """Form in which OR operations may have AND operands, but not the reverse.
102 For example, ``A OR (B AND C)`` is in disjunctive normal form, but
103 ``A AND (B OR C)`` and ``A OR (B AND (C OR D))`` are not.
104 """
106 @property
107 def inner(self) -> LogicalBinaryOperator:
108 """The operator that is not permitted to contain operations of the
109 other type in this form.
111 Note that this operation may still appear as the outermost operator in
112 an expression in this form if there are no operations of the other
113 type.
114 """
115 return LogicalBinaryOperator(not self.value)
117 @property
118 def outer(self) -> LogicalBinaryOperator:
119 """The operator that is not permitted to be contained by operations of
120 the other type in this form.
122 Note that an operation of this type is not necessarily the outermost
123 operator in an expression in this form; the expression need not contain
124 any operations of this type.
125 """
126 return LogicalBinaryOperator(self.value)
128 def allows(self, *, inner: LogicalBinaryOperator, outer: LogicalBinaryOperator) -> bool:
129 """Test whether this form allows the given operator relationships.
131 Parameters
132 ----------
133 inner : `LogicalBinaryOperator`
134 Inner operator in an expression. May be the same as ``outer``.
135 outer : `LogicalBinaryOperator`
136 Outer operator in an expression. May be the same as ``inner``.
138 Returns
139 -------
140 allowed : `bool`
141 Whether this operation relationship is allowed.
142 """
143 return inner == outer or outer is self.outer
146_T = TypeVar("_T")
147_U = TypeVar("_U")
148_V = TypeVar("_V")
151class NormalFormVisitor(Generic[_T, _U, _V]):
152 """A visitor interface for `NormalFormExpression`.
154 A visitor implementation may inherit from both `NormalFormVisitor` and
155 `TreeVisitor` and implement `visitBranch` as ``node.visit(self)`` to
156 visit each node of the entire tree (or similarly with composition, etc).
157 In this case, `TreeVisitor.visitBinaryOp` will never be called with a
158 logical OR or AND operation, because these will all have been moved into
159 calls to `visitInner` and `visitOuter` instead.
161 See also `NormalFormExpression.visit`.
162 """
164 @abstractmethod
165 def visitBranch(self, node: Node) -> _T:
166 """Visit a regular `Node` and its child nodes.
168 Parameters
169 ----------
170 node : `Node`
171 A branch of the expression tree that contains no AND or OR
172 operations.
174 Returns
175 -------
176 result
177 Implementation-defined result to be gathered and passed to
178 `visitInner`.
179 """
180 raise NotImplementedError()
182 @abstractmethod
183 def visitInner(self, branches: Sequence[_T], form: NormalForm) -> _U:
184 """Visit a sequence of inner OR (for `~NormalForm.CONJUNCTIVE` form)
185 or AND (for `~NormalForm.DISJUNCTIVE`) operands.
187 Parameters
188 ----------
189 branches : `~collections.abc.Sequence`
190 Sequence of tuples, where the first element in each tuple is the
191 result of a call to `visitBranch`, and the second is the `Node` on
192 which `visitBranch` was called.
193 form : `NormalForm`
194 Form this expression is in. ``form.inner`` is the operator that
195 joins the operands in ``branches``.
197 Returns
198 -------
199 result
200 Implementation-defined result to be gathered and passed to
201 `visitOuter`.
202 """
203 raise NotImplementedError()
205 @abstractmethod
206 def visitOuter(self, branches: Sequence[_U], form: NormalForm) -> _V:
207 """Visit the sequence of outer AND (for `~NormalForm.CONJUNCTIVE` form)
208 or OR (for `~NormalForm.DISJUNCTIVE`) operands.
210 Parameters
211 ----------
212 branches : `~collections.abc.Sequence`
213 Sequence of return values from calls to `visitInner`.
214 form : `NormalForm`
215 Form this expression is in. ``form.outer`` is the operator that
216 joins the operands in ``branches``.
218 Returns
219 -------
220 result
221 Implementation-defined result to be returned by
222 `NormalFormExpression.visitNormalForm`.
223 """
224 raise NotImplementedError()
227class NormalFormExpression:
228 """A boolean expression in a standard normal form.
230 Most code should use `fromTree` to construct new instances instead of
231 calling the constructor directly. See `NormalForm` for a description of
232 the two forms.
234 Parameters
235 ----------
236 nodes : `~collections.abc.Sequence` [ `~collections.abc.Sequence` \
237 [ `Node` ] ]
238 Non-AND, non-OR branches of three tree, with the AND and OR operations
239 combining them represented by position in the nested sequence - the
240 inner sequence is combined via ``form.inner``, and the outer sequence
241 combines those via ``form.outer``.
242 form : `NormalForm`
243 Enumeration value indicating the form this expression is in.
244 """
246 def __init__(self, nodes: Sequence[Sequence[Node]], form: NormalForm):
247 self._form = form
248 self._nodes = nodes
250 def __str__(self) -> str:
251 return str(self.toTree())
253 @staticmethod
254 def fromTree(root: Node, form: NormalForm) -> NormalFormExpression:
255 """Construct a `NormalFormExpression` by normalizing an arbitrary
256 expression tree.
258 Parameters
259 ----------
260 root : `Node`
261 Root of the tree to be normalized.
262 form : `NormalForm`
263 Enumeration value indicating the form to normalize to.
265 Notes
266 -----
267 Converting an arbitrary boolean expression to either normal form is an
268 NP-hard problem, and this is a brute-force algorithm. I'm not sure
269 what its actual algorithmic scaling is, but it'd definitely be a bad
270 idea to attempt to normalize an expression with hundreds or thousands
271 of clauses.
272 """
273 wrapper = root.visit(TransformationVisitor()).normalize(form)
274 nodes = []
275 for outerOperands in wrapper.flatten(form.outer):
276 nodes.append([w.unwrap() for w in outerOperands.flatten(form.inner)])
277 return NormalFormExpression(nodes, form=form)
279 @property
280 def form(self) -> NormalForm:
281 """Enumeration value indicating the form this expression is in."""
282 return self._form
284 def visit(self, visitor: NormalFormVisitor[_T, _U, _V]) -> _V:
285 """Apply a visitor to the expression, explicitly taking advantage of
286 the special structure guaranteed by the normal forms.
288 Parameters
289 ----------
290 visitor : `NormalFormVisitor`
291 Visitor object to apply.
293 Returns
294 -------
295 result
296 Return value from calling ``visitor.visitOuter`` after visiting
297 all other nodes.
298 """
299 visitedOuterBranches: list[_U] = []
300 for nodeInnerBranches in self._nodes:
301 visitedInnerBranches = [visitor.visitBranch(node) for node in nodeInnerBranches]
302 visitedOuterBranches.append(visitor.visitInner(visitedInnerBranches, self.form))
303 return visitor.visitOuter(visitedOuterBranches, self.form)
305 def toTree(self) -> Node:
306 """Convert ``self`` back into an equivalent tree, preserving the
307 form of the boolean expression but representing it in memory as
308 a tree.
310 Returns
311 -------
312 tree : `Node`
313 Root of the tree equivalent to ``self``.
314 """
315 visitor = TreeReconstructionVisitor()
316 return self.visit(visitor)
319class PrecedenceTier(enum.Enum):
320 """An enumeration used to track operator precedence.
322 This enum is currently used only to inject parentheses into boolean
323 logic that has been manipulated into a new form, and because those
324 parentheses are only used for stringification, the goal here is human
325 readability, not precision.
327 Lower enum values represent tighter binding, but code deciding whether to
328 inject parentheses should always call `needsParens` instead of comparing
329 values directly.
330 """
332 TOKEN = 0
333 """Precedence tier for literals, identifiers, and expressions already in
334 parentheses.
335 """
337 UNARY = 1
338 """Precedence tier for unary operators, which always bind more tightly
339 than any binary operator.
340 """
342 VALUE_BINARY_OP = 2
343 """Precedence tier for binary operators that return non-boolean values.
344 """
346 COMPARISON = 3
347 """Precedence tier for binary comparison operators that accept non-boolean
348 values and return boolean values.
349 """
351 AND = 4
352 """Precedence tier for logical AND.
353 """
355 OR = 5
356 """Precedence tier for logical OR.
357 """
359 @classmethod
360 def needsParens(cls, outer: PrecedenceTier, inner: PrecedenceTier) -> bool:
361 """Test whether parentheses should be added around an operand.
363 Parameters
364 ----------
365 outer : `PrecedenceTier`
366 Precedence tier for the operation.
367 inner : `PrecedenceTier`
368 Precedence tier for the operand.
370 Returns
371 -------
372 needed : `bool`
373 If `True`, parentheses should be added.
375 Notes
376 -----
377 This method special cases logical binary operators for readability,
378 adding parentheses around ANDs embedded in ORs, and avoiding them in
379 chains of the same operator. If it is ever actually used with other
380 binary operators as ``outer``, those would probably merit similar
381 attention (or a totally new approach); its current approach would
382 aggressively add a lot of unfortunate parentheses because (aside from
383 this special-casing) it doesn't know about commutativity.
385 In fact, this method is rarely actually used for logical binary
386 operators either; the `LogicalBinaryOperation.unwrap` method that calls
387 it is never invoked by `NormalFormExpression` (the main public
388 interface), because those operators are flattened out (see
389 `TransformationWrapper.flatten`) instead. Parentheses are instead
390 added there by `TreeReconstructionVisitor`, which is simpler because
391 the structure of operators is restricted.
392 """
393 if outer is cls.OR and inner is cls.AND:
394 return True
395 if outer is cls.OR and inner is cls.OR:
396 return False
397 if outer is cls.AND and inner is cls.AND:
398 return False
399 return outer.value <= inner.value
402BINARY_OPERATOR_PRECEDENCE = {
403 "=": PrecedenceTier.COMPARISON,
404 "!=": PrecedenceTier.COMPARISON,
405 "<": PrecedenceTier.COMPARISON,
406 "<=": PrecedenceTier.COMPARISON,
407 ">": PrecedenceTier.COMPARISON,
408 ">=": PrecedenceTier.COMPARISON,
409 "OVERLAPS": PrecedenceTier.COMPARISON,
410 "+": PrecedenceTier.VALUE_BINARY_OP,
411 "-": PrecedenceTier.VALUE_BINARY_OP,
412 "*": PrecedenceTier.VALUE_BINARY_OP,
413 "/": PrecedenceTier.VALUE_BINARY_OP,
414 "AND": PrecedenceTier.AND,
415 "OR": PrecedenceTier.OR,
416}
419class TransformationWrapper(ABC):
420 """A base class for `Node` wrappers that can be used to transform boolean
421 operator expressions.
423 Notes
424 -----
425 `TransformationWrapper` instances should only be directly constructed by
426 each other or `TransformationVisitor`. No new subclasses beyond those in
427 this module should be added.
429 While `TransformationWrapper` and its subclasses contain the machinery
430 for transforming expressions to normal forms, it does not provide a
431 convenient interface for working with expressions in those forms; most code
432 should just use `NormalFormExpression` (which delegates to
433 `TransformationWrapper` internally) instead.
434 """
436 __slots__ = ()
438 @abstractmethod
439 def __str__(self) -> str:
440 raise NotImplementedError()
442 @property
443 @abstractmethod
444 def precedence(self) -> PrecedenceTier:
445 """Return the precedence tier for this node (`PrecedenceTier`).
447 Notes
448 -----
449 This is only used when reconstructing a full `Node` tree in `unwrap`
450 implementations, and only to inject parenthesis that are necessary for
451 correct stringification but nothing else (because the tree structure
452 itself embeds the correct order of operations already).
453 """
454 raise NotImplementedError()
456 @abstractmethod
457 def not_(self) -> TransformationWrapper:
458 """Return a wrapper that represents the logical NOT of ``self``.
460 Returns
461 -------
462 wrapper : `TransformationWrapper`
463 A wrapper that represents the logical NOT of ``self``.
464 """
465 raise NotImplementedError()
467 def satisfies(self, form: NormalForm) -> bool:
468 """Test whether this expressions is already in a normal form.
470 The default implementation is appropriate for "atomic" classes that
471 never contain any AND or OR operations at all.
473 Parameters
474 ----------
475 form : `NormalForm`
476 Enumeration indicating the form to test for.
478 Returns
479 -------
480 satisfies : `bool`
481 Whether ``self`` satisfies the requirements of ``form``.
482 """
483 return True
485 def normalize(self, form: NormalForm) -> TransformationWrapper:
486 """Return an expression equivalent to ``self`` in a normal form.
488 The default implementation is appropriate for "atomic" classes that
489 never contain any AND or OR operations at all.
491 Parameters
492 ----------
493 form : `NormalForm`
494 Enumeration indicating the form to convert to.
496 Returns
497 -------
498 normalized : `TransformationWrapper`
499 An expression equivalent to ``self`` for which
500 ``normalized.satisfies(form)`` returns `True`.
502 Notes
503 -----
504 Converting an arbitrary boolean expression to either normal form is an
505 NP-hard problem, and this is a brute-force algorithm. I'm not sure
506 what its actual algorithmic scaling is, but it'd definitely be a bad
507 idea to attempt to normalize an expression with hundreds or thousands
508 of clauses.
509 """
510 return self
512 def flatten(self, operator: LogicalBinaryOperator) -> Iterator[TransformationWrapper]:
513 """Recursively flatten the operands of any nested operators of the
514 given type.
516 For an expression like ``(A AND ((B OR C) AND D)`` (with
517 ``operator == AND``), `flatten` yields ``A, (B OR C), D``.
519 The default implementation is appropriate for "atomic" classes that
520 never contain any AND or OR operations at all.
522 Parameters
523 ----------
524 operator : `LogicalBinaryOperator`
525 Operator whose operands to flatten.
527 Returns
528 -------
529 operands : `~collections.abc.Iterator` [ `TransformationWrapper` ]
530 Operands that, if combined with ``operator``, yield an expression
531 equivalent to ``self``.
532 """
533 yield self
535 def _satisfiesDispatch(
536 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
537 ) -> bool:
538 """Test whether ``operator.apply(self, other)`` is in a normal form.
540 The default implementation is appropriate for classes that never
541 contain any AND or OR operations at all.
543 Parameters
544 ----------
545 operator : `LogicalBinaryOperator`
546 Operator for the operation being tested.
547 other : `TransformationWrapper`
548 Other operand for the operation being tested.
549 form : `NormalForm`
550 Normal form being tested for.
552 Returns
553 -------
554 satisfies : `bool`
555 Whether ``operator.apply(self, other)`` satisfies the requirements
556 of ``form``.
558 Notes
559 -----
560 Caller guarantees that ``self`` and ``other`` are already normalized.
561 """
562 return other._satisfiesDispatchAtomic(operator, self, form=form)
564 def _normalizeDispatch(
565 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
566 ) -> TransformationWrapper:
567 """Return an expression equivalent to ``operator.apply(self, other)``
568 in a normal form.
570 The default implementation is appropriate for classes that never
571 contain any AND or OR operations at all.
573 Parameters
574 ----------
575 operator : `LogicalBinaryOperator`
576 Operator for the operation being transformed.
577 other : `TransformationWrapper`
578 Other operand for the operation being transformed.
579 form : `NormalForm`
580 Normal form being transformed to.
582 Returns
583 -------
584 normalized : `TransformationWrapper`
585 An expression equivalent to ``operator.apply(self, other)``
586 for which ``normalized.satisfies(form)`` returns `True`.
588 Notes
589 -----
590 Caller guarantees that ``self`` and ``other`` are already normalized.
591 """
592 return other._normalizeDispatchAtomic(operator, self, form=form)
594 def _satisfiesDispatchAtomic(
595 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
596 ) -> bool:
597 """Test whather ``operator.apply(other, self)`` is in a normal form.
599 The default implementation is appropriate for "atomic" classes that
600 never contain any AND or OR operations at all.
602 Parameters
603 ----------
604 operator : `LogicalBinaryOperator`
605 Operator for the operation being tested.
606 other : `TransformationWrapper`
607 Other operand for the operation being tested.
608 form : `NormalForm`
609 Normal form being tested for.
611 Returns
612 -------
613 satisfies : `bool`
614 Whether ``operator.apply(other, self)`` satisfies the requirements
615 of ``form``.
617 Notes
618 -----
619 Should only be called by `_satisfiesDispatch` implementations; this
620 guarantees that ``other`` is atomic and ``self`` is normalized.
621 """
622 return True
624 def _normalizeDispatchAtomic(
625 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
626 ) -> TransformationWrapper:
627 """Return an expression equivalent to ``operator.apply(other, self)``,
628 in a normal form.
630 The default implementation is appropriate for "atomic" classes that
631 never contain any AND or OR operations at all.
633 Parameters
634 ----------
635 operator : `LogicalBinaryOperator`
636 Operator for the operation being transformed.
637 other : `TransformationWrapper`
638 Other operand for the operation being transformed.
639 form : `NormalForm`
640 Normal form being transformed to.
642 Returns
643 -------
644 normalized : `TransformationWrapper`
645 An expression equivalent to ``operator.apply(other, self)``
646 for which ``normalized.satisfies(form)`` returns `True`.
648 Notes
649 -----
650 Should only be called by `_normalizeDispatch` implementations; this
651 guarantees that ``other`` is atomic and ``self`` is normalized.
652 """
653 return operator.apply(other, self)
655 def _satisfiesDispatchBinary(
656 self,
657 outer: LogicalBinaryOperator,
658 lhs: TransformationWrapper,
659 inner: LogicalBinaryOperator,
660 rhs: TransformationWrapper,
661 *,
662 form: NormalForm,
663 ) -> bool:
664 """Test whether ``outer.apply(self, inner.apply(lhs, rhs))`` is in a
665 normal form.
667 The default implementation is appropriate for "atomic" classes that
668 never contain any AND or OR operations at all.
670 Parameters
671 ----------
672 outer : `LogicalBinaryOperator`
673 Outer operator for the expression being tested.
674 lhs : `TransformationWrapper`
675 One inner operand for the expression being tested.
676 inner : `LogicalBinaryOperator`
677 Inner operator for the expression being tested. This may or may
678 not be the same as ``outer``.
679 rhs: `TransformationWrapper`
680 The other inner operand for the expression being tested.
681 form : `NormalForm`
682 Normal form being transformed to.
684 Returns
685 -------
686 satisfies : `bool`
687 Whether ``outer.apply(self, inner.apply(lhs, rhs))`` satisfies the
688 requirements of ``form``.
690 Notes
691 -----
692 Should only be called by `_satisfiesDispatch` implementations; this
693 guarantees that ``self``, ``lhs``, and ``rhs`` are all normalized.
694 """
695 return form.allows(inner=inner, outer=outer)
697 def _normalizeDispatchBinary(
698 self,
699 outer: LogicalBinaryOperator,
700 lhs: TransformationWrapper,
701 inner: LogicalBinaryOperator,
702 rhs: TransformationWrapper,
703 *,
704 form: NormalForm,
705 ) -> TransformationWrapper:
706 """Return an expression equivalent to
707 ``outer.apply(self, inner.apply(lhs, rhs))``, meeting the guarantees of
708 `normalize`.
710 The default implementation is appropriate for "atomic" classes that
711 never contain any AND or OR operations at all.
713 Parameters
714 ----------
715 outer : `LogicalBinaryOperator`
716 Outer operator for the expression being transformed.
717 lhs : `TransformationWrapper`
718 One inner operand for the expression being transformed.
719 inner : `LogicalBinaryOperator`
720 Inner operator for the expression being transformed.
721 rhs: `TransformationWrapper`
722 The other inner operand for the expression being transformed.
723 form : `NormalForm`
724 Normal form being transformed to.
726 Returns
727 -------
728 normalized : `TransformationWrapper`
729 An expression equivalent to
730 ``outer.apply(self, inner.apply(lhs, rhs))`` for which
731 ``normalized.satisfies(form)`` returns `True`.
733 Notes
734 -----
735 Should only be called by `_normalizeDispatch` implementations; this
736 guarantees that ``self``, ``lhs``, and ``rhs`` are all normalized.
737 """
738 if form.allows(inner=inner, outer=outer):
739 return outer.apply(inner.apply(lhs, rhs), self)
740 else:
741 return inner.apply(
742 outer.apply(lhs, self).normalize(form),
743 outer.apply(rhs, self).normalize(form),
744 )
746 @abstractmethod
747 def unwrap(self) -> Node:
748 """Return an transformed expression tree.
750 Return:
751 ------
752 tree : `Node`
753 Tree node representing the same expression (and form) as ``self``.
754 """
755 raise NotImplementedError()
758class Opaque(TransformationWrapper):
759 """A `TransformationWrapper` implementation for tree nodes that do not need
760 to be modified in boolean expression transformations.
762 This includes all identifiers, literals, and operators whose arguments are
763 not boolean.
765 Parameters
766 ----------
767 node : `Node`
768 Node wrapped by ``self``.
769 precedence : `PrecedenceTier`
770 Enumeration indicating how tightly this node is bound.
771 """
773 def __init__(self, node: Node, precedence: PrecedenceTier):
774 self._node = node
775 self._precedence = precedence
777 __slots__ = ("_node", "_precedence")
779 def __str__(self) -> str:
780 return str(self._node)
782 @property
783 def precedence(self) -> PrecedenceTier:
784 # Docstring inherited from `TransformationWrapper`.
785 return self._precedence
787 def not_(self) -> TransformationWrapper:
788 # Docstring inherited from `TransformationWrapper`.
789 return LogicalNot(self)
791 def unwrap(self) -> Node:
792 # Docstring inherited from `TransformationWrapper`.
793 return self._node
796class LogicalNot(TransformationWrapper):
797 """A `TransformationWrapper` implementation for logical NOT operations.
799 Parameters
800 ----------
801 operand : `TransformationWrapper`
802 Wrapper representing the operand of the NOT operation.
804 Notes
805 -----
806 Instances should always be created by calling `not_` on an existing
807 `TransformationWrapper` instead of calling `LogicalNot` directly.
808 `LogicalNot` should only be called directly by `Opaque.not_`. This
809 guarantees that double-negatives are simplified away and NOT operations
810 are moved inside any OR and AND operations at construction.
811 """
813 def __init__(self, operand: Opaque):
814 self._operand = operand
816 __slots__ = ("_operand",)
818 def __str__(self) -> str:
819 return f"not({self._operand})"
821 @property
822 def precedence(self) -> PrecedenceTier:
823 # Docstring inherited from `TransformationWrapper`.
824 return PrecedenceTier.UNARY
826 def not_(self) -> TransformationWrapper:
827 # Docstring inherited from `TransformationWrapper`.
828 return self._operand
830 def unwrap(self) -> Node:
831 # Docstring inherited from `TransformationWrapper`.
832 node = self._operand.unwrap()
833 if PrecedenceTier.needsParens(self.precedence, self._operand.precedence):
834 node = Parens(node)
835 return UnaryOp("NOT", node)
838class LogicalBinaryOperation(TransformationWrapper):
839 """A `TransformationWrapper` implementation for logical OR and NOT
840 implementations.
842 Parameters
843 ----------
844 lhs : `TransformationWrapper`
845 First operand.
846 operator : `LogicalBinaryOperator`
847 Enumeration representing the operator.
848 rhs : `TransformationWrapper`
849 Second operand.
850 """
852 def __init__(
853 self, lhs: TransformationWrapper, operator: LogicalBinaryOperator, rhs: TransformationWrapper
854 ):
855 self._lhs = lhs
856 self._operator = operator
857 self._rhs = rhs
858 self._satisfiesCache: dict[NormalForm, bool] = {}
860 __slots__ = ("_lhs", "_operator", "_rhs", "_satisfiesCache")
862 def __str__(self) -> str:
863 return f"{self._operator.name.lower()}({self._lhs}, {self._rhs})"
865 @property
866 def precedence(self) -> PrecedenceTier:
867 # Docstring inherited from `TransformationWrapper`.
868 return BINARY_OPERATOR_PRECEDENCE[self._operator.name]
870 def not_(self) -> TransformationWrapper:
871 # Docstring inherited from `TransformationWrapper`.
872 return LogicalBinaryOperation(
873 self._lhs.not_(),
874 LogicalBinaryOperator(not self._operator.value),
875 self._rhs.not_(),
876 )
878 def satisfies(self, form: NormalForm) -> bool:
879 # Docstring inherited from `TransformationWrapper`.
880 r = self._satisfiesCache.get(form)
881 if r is None:
882 r = (
883 self._lhs.satisfies(form)
884 and self._rhs.satisfies(form)
885 and self._lhs._satisfiesDispatch(self._operator, self._rhs, form=form)
886 )
887 self._satisfiesCache[form] = r
888 return r
890 def normalize(self, form: NormalForm) -> TransformationWrapper:
891 # Docstring inherited from `TransformationWrapper`.
892 if self.satisfies(form):
893 return self
894 lhs = self._lhs.normalize(form)
895 rhs = self._rhs.normalize(form)
896 return lhs._normalizeDispatch(self._operator, rhs, form=form)
898 def flatten(self, operator: LogicalBinaryOperator) -> Iterator[TransformationWrapper]:
899 # Docstring inherited from `TransformationWrapper`.
900 if operator is self._operator:
901 yield from self._lhs.flatten(operator)
902 yield from self._rhs.flatten(operator)
903 else:
904 yield self
906 def _satisfiesDispatch(
907 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
908 ) -> bool:
909 # Docstring inherited from `TransformationWrapper`.
910 return other._satisfiesDispatchBinary(operator, self._lhs, self._operator, self._rhs, form=form)
912 def _normalizeDispatch(
913 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
914 ) -> TransformationWrapper:
915 # Docstring inherited from `TransformationWrapper`.
916 return other._normalizeDispatchBinary(operator, self._lhs, self._operator, self._rhs, form=form)
918 def _satisfiesDispatchAtomic(
919 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
920 ) -> bool:
921 # Docstring inherited from `TransformationWrapper`.
922 return form.allows(outer=operator, inner=self._operator)
924 def _normalizeDispatchAtomic(
925 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
926 ) -> TransformationWrapper:
927 # Docstring inherited from `TransformationWrapper`.
928 # Normalizes an expression of the form:
929 #
930 # operator.apply(
931 # other,
932 # self._operator.apply(self._lhs, self._rhs),
933 # )
934 #
935 if form.allows(outer=operator, inner=self._operator):
936 return operator.apply(other, self)
937 else:
938 return self._operator.apply(
939 operator.apply(other, self._lhs).normalize(form),
940 operator.apply(other, self._rhs).normalize(form),
941 )
943 def _satisfiesDispatchBinary(
944 self,
945 outer: LogicalBinaryOperator,
946 lhs: TransformationWrapper,
947 inner: LogicalBinaryOperator,
948 rhs: TransformationWrapper,
949 *,
950 form: NormalForm,
951 ) -> bool:
952 # Docstring inherited from `TransformationWrapper`.
953 return form.allows(outer=outer, inner=inner) and form.allows(outer=outer, inner=self._operator)
955 def _normalizeDispatchBinary(
956 self,
957 outer: LogicalBinaryOperator,
958 lhs: TransformationWrapper,
959 inner: LogicalBinaryOperator,
960 rhs: TransformationWrapper,
961 *,
962 form: NormalForm,
963 ) -> TransformationWrapper:
964 # Docstring inherited from `TransformationWrapper`.
965 # Normalizes an expression of the form:
966 #
967 # outer.apply(
968 # inner.apply(lhs, rhs),
969 # self._operator.apply(self._lhs, self._rhs),
970 # )
971 #
972 if form.allows(inner=inner, outer=outer):
973 other = inner.apply(lhs, rhs)
974 if form.allows(inner=self._operator, outer=outer):
975 return outer.apply(other, self)
976 else:
977 return self._operator.apply(
978 outer.apply(other, self._lhs).normalize(form),
979 outer.apply(other, self._rhs).normalize(form),
980 )
981 else:
982 if form.allows(inner=self._operator, outer=outer):
983 return inner.apply(
984 outer.apply(lhs, self).normalize(form), outer.apply(rhs, self).normalize(form)
985 )
986 else:
987 assert form.allows(inner=inner, outer=self._operator)
988 return self._operator.apply(
989 inner.apply(
990 outer.apply(lhs, self._lhs).normalize(form),
991 outer.apply(lhs, self._rhs).normalize(form),
992 ),
993 inner.apply(
994 outer.apply(rhs, self._lhs).normalize(form),
995 outer.apply(rhs, self._rhs).normalize(form),
996 ),
997 )
999 def unwrap(self) -> Node:
1000 # Docstring inherited from `TransformationWrapper`.
1001 lhsNode = self._lhs.unwrap()
1002 if PrecedenceTier.needsParens(self.precedence, self._lhs.precedence):
1003 lhsNode = Parens(lhsNode)
1004 rhsNode = self._rhs.unwrap()
1005 if PrecedenceTier.needsParens(self.precedence, self._rhs.precedence):
1006 rhsNode = Parens(rhsNode)
1007 return BinaryOp(lhsNode, self._operator.name, rhsNode)
1010class TransformationVisitor(TreeVisitor[TransformationWrapper]):
1011 """A `TreeVisitor` implementation that constructs a `TransformationWrapper`
1012 tree when applied to a `Node` tree.
1013 """
1015 def visitNumericLiteral(self, value: str, node: Node) -> TransformationWrapper:
1016 # Docstring inherited from TreeVisitor.visitNumericLiteral
1017 return Opaque(node, PrecedenceTier.TOKEN)
1019 def visitStringLiteral(self, value: str, node: Node) -> TransformationWrapper:
1020 # Docstring inherited from TreeVisitor.visitStringLiteral
1021 return Opaque(node, PrecedenceTier.TOKEN)
1023 def visitTimeLiteral(self, value: astropy.time.Time, node: Node) -> TransformationWrapper:
1024 # Docstring inherited from TreeVisitor.visitTimeLiteral
1025 return Opaque(node, PrecedenceTier.TOKEN)
1027 def visitRangeLiteral(
1028 self, start: int, stop: int, stride: int | None, node: Node
1029 ) -> TransformationWrapper:
1030 # Docstring inherited from TreeVisitor.visitRangeLiteral
1031 return Opaque(node, PrecedenceTier.TOKEN)
1033 def visitIdentifier(self, name: str, node: Node) -> TransformationWrapper:
1034 # Docstring inherited from TreeVisitor.visitIdentifier
1035 return Opaque(node, PrecedenceTier.TOKEN)
1037 def visitUnaryOp(
1038 self,
1039 operator: str,
1040 operand: TransformationWrapper,
1041 node: Node,
1042 ) -> TransformationWrapper:
1043 # Docstring inherited from TreeVisitor.visitUnaryOp
1044 if operator == "NOT":
1045 return operand.not_()
1046 else:
1047 return Opaque(node, PrecedenceTier.UNARY)
1049 def visitBinaryOp(
1050 self,
1051 operator: str,
1052 lhs: TransformationWrapper,
1053 rhs: TransformationWrapper,
1054 node: Node,
1055 ) -> TransformationWrapper:
1056 # Docstring inherited from TreeVisitor.visitBinaryOp
1057 logical = LogicalBinaryOperator.__members__.get(operator)
1058 if logical is not None:
1059 return LogicalBinaryOperation(lhs, logical, rhs)
1060 return Opaque(node, BINARY_OPERATOR_PRECEDENCE[operator])
1062 def visitIsIn(
1063 self,
1064 lhs: TransformationWrapper,
1065 values: list[TransformationWrapper],
1066 not_in: bool,
1067 node: Node,
1068 ) -> TransformationWrapper:
1069 # Docstring inherited from TreeVisitor.visitIsIn
1070 return Opaque(node, PrecedenceTier.COMPARISON)
1072 def visitParens(self, expression: TransformationWrapper, node: Node) -> TransformationWrapper:
1073 # Docstring inherited from TreeVisitor.visitParens
1074 return expression
1076 def visitTupleNode(self, items: tuple[TransformationWrapper, ...], node: Node) -> TransformationWrapper:
1077 # Docstring inherited from TreeVisitor.visitTupleNode
1078 return Opaque(node, PrecedenceTier.TOKEN)
1080 def visitPointNode(
1081 self, ra: TransformationWrapper, dec: TransformationWrapper, node: Node
1082 ) -> TransformationWrapper:
1083 # Docstring inherited from TreeVisitor.visitPointNode
1084 raise NotImplementedError("POINT() function is not supported yet")
1087class TreeReconstructionVisitor(NormalFormVisitor[Node, Node, Node]):
1088 """A `NormalFormVisitor` that reconstructs complete expression tree.
1090 Outside code should use `NormalFormExpression.toTree` (which delegates to
1091 this visitor) instead.
1092 """
1094 def visitBranch(self, node: Node) -> Node:
1095 # Docstring inherited from NormalFormVisitor.
1096 return node
1098 def _visitSequence(self, branches: Sequence[Node], operator: LogicalBinaryOperator) -> Node:
1099 """Implement common recursive implementation for `visitInner` and
1100 `visitOuter`.
1102 Parameters
1103 ----------
1104 branches : `~collections.abc.Sequence`
1105 Sequence of return values from calls to `visitBranch`, representing
1106 a visited set of operands combined in the expression by
1107 ``operator``.
1108 operator : `LogicalBinaryOperator`
1109 Operator that joins the elements of ``branches``.
1111 Returns
1112 -------
1113 result
1114 Result of the final call to ``visitBranch(node)``.
1115 node : `Node`
1116 Hierarchical expression tree equivalent to joining ``branches``
1117 with ``operator``.
1118 """
1119 first, *rest = branches
1120 if not rest:
1121 return first
1122 merged = self._visitSequence(rest, operator)
1123 node = BinaryOp(first, operator.name, merged)
1124 return self.visitBranch(node)
1126 def visitInner(self, branches: Sequence[Node], form: NormalForm) -> Node:
1127 # Docstring inherited from NormalFormVisitor.
1128 node = self._visitSequence(branches, form.inner)
1129 if len(branches) > 1:
1130 node = Parens(node)
1131 return node
1133 def visitOuter(self, branches: Sequence[Node], form: NormalForm) -> Node:
1134 # Docstring inherited from NormalFormVisitor.
1135 node = self._visitSequence(branches, form.outer)
1136 if isinstance(node, Parens):
1137 node = node.expr
1138 return node