Coverage for python/lsst/daf/butler/registry/queries/expressions/normalForm.py: 48%
253 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-07 02:46 -0700
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-07 02:46 -0700
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <https://www.gnu.org/licenses/>.
28from __future__ import annotations
30__all__ = (
31 "NormalForm",
32 "NormalFormExpression",
33 "NormalFormVisitor",
34)
36import enum
37from abc import ABC, abstractmethod
38from collections.abc import Iterator, Sequence
39from typing import Generic, TypeVar
41import astropy.time
43from .parser import BinaryOp, Node, Parens, TreeVisitor, UnaryOp
46class LogicalBinaryOperator(enum.Enum):
47 """Enumeration for logical binary operators.
49 These intentionally have the same names (including capitalization) as the
50 string binary operators used in the parser itself. Boolean values are
51 used to enable generic code that works on either operator (particularly
52 ``LogicalBinaryOperator(not self)`` as a way to get the other operator).
54 Which is `True` and which is `False` is just a convention, but one shared
55 by `LogicalBinaryOperator` and `NormalForm`.
56 """
58 AND = True
59 OR = False
61 def apply(self, lhs: TransformationWrapper, rhs: TransformationWrapper) -> LogicalBinaryOperation:
62 """Return a `TransformationWrapper` object representing this operator
63 applied to the given operands.
65 This is simply syntactic sugar for the `LogicalBinaryOperation`
66 constructor.
68 Parameters
69 ----------
70 lhs : `TransformationWrapper`
71 First operand.
72 rhs : `TransformationWrapper`
73 Second operand.
75 Returns
76 -------
77 operation : `LogicalBinaryOperation`
78 Object representing the operation.
79 """
80 return LogicalBinaryOperation(lhs, self, rhs)
83class NormalForm(enum.Enum):
84 """Enumeration for boolean normal forms.
86 Both normal forms require all NOT operands to be moved "inside" any AND
87 or OR operations (i.e. by applying DeMorgan's laws), with either all AND
88 operations or all OR operations appearing outside the other (`CONJUNCTIVE`,
89 and `DISJUNCTIVE`, respectively).
91 Boolean values are used here to enable generic code that works on either
92 form, and for interoperability with `LogicalBinaryOperator`.
94 Which is `True` and which is `False` is just a convention, but one shared
95 by `LogicalBinaryOperator` and `NormalForm`.
96 """
98 CONJUNCTIVE = True
99 """Form in which AND operations may have OR operands, but not the reverse.
101 For example, ``A AND (B OR C)`` is in conjunctive normal form, but
102 ``A OR (B AND C)`` and ``A AND (B OR (C AND D))`` are not.
103 """
105 DISJUNCTIVE = False
106 """Form in which OR operations may have AND operands, but not the reverse.
108 For example, ``A OR (B AND C)`` is in disjunctive normal form, but
109 ``A AND (B OR C)`` and ``A OR (B AND (C OR D))`` are not.
110 """
112 @property
113 def inner(self) -> LogicalBinaryOperator:
114 """The operator that is not permitted to contain operations of the
115 other type in this form.
117 Note that this operation may still appear as the outermost operator in
118 an expression in this form if there are no operations of the other
119 type.
120 """
121 return LogicalBinaryOperator(not self.value)
123 @property
124 def outer(self) -> LogicalBinaryOperator:
125 """The operator that is not permitted to be contained by operations of
126 the other type in this form.
128 Note that an operation of this type is not necessarily the outermost
129 operator in an expression in this form; the expression need not contain
130 any operations of this type.
131 """
132 return LogicalBinaryOperator(self.value)
134 def allows(self, *, inner: LogicalBinaryOperator, outer: LogicalBinaryOperator) -> bool:
135 """Test whether this form allows the given operator relationships.
137 Parameters
138 ----------
139 inner : `LogicalBinaryOperator`
140 Inner operator in an expression. May be the same as ``outer``.
141 outer : `LogicalBinaryOperator`
142 Outer operator in an expression. May be the same as ``inner``.
144 Returns
145 -------
146 allowed : `bool`
147 Whether this operation relationship is allowed.
148 """
149 return inner == outer or outer is self.outer
152_T = TypeVar("_T")
153_U = TypeVar("_U")
154_V = TypeVar("_V")
157class NormalFormVisitor(Generic[_T, _U, _V]):
158 """A visitor interface for `NormalFormExpression`.
160 A visitor implementation may inherit from both `NormalFormVisitor` and
161 `TreeVisitor` and implement `visitBranch` as ``node.visit(self)`` to
162 visit each node of the entire tree (or similarly with composition, etc).
163 In this case, `TreeVisitor.visitBinaryOp` will never be called with a
164 logical OR or AND operation, because these will all have been moved into
165 calls to `visitInner` and `visitOuter` instead.
167 See also `NormalFormExpression.visit`.
168 """
170 @abstractmethod
171 def visitBranch(self, node: Node) -> _T:
172 """Visit a regular `Node` and its child nodes.
174 Parameters
175 ----------
176 node : `Node`
177 A branch of the expression tree that contains no AND or OR
178 operations.
180 Returns
181 -------
182 result
183 Implementation-defined result to be gathered and passed to
184 `visitInner`.
185 """
186 raise NotImplementedError()
188 @abstractmethod
189 def visitInner(self, branches: Sequence[_T], form: NormalForm) -> _U:
190 """Visit a sequence of inner OR (for `~NormalForm.CONJUNCTIVE` form)
191 or AND (for `~NormalForm.DISJUNCTIVE`) operands.
193 Parameters
194 ----------
195 branches : `~collections.abc.Sequence`
196 Sequence of tuples, where the first element in each tuple is the
197 result of a call to `visitBranch`, and the second is the `Node` on
198 which `visitBranch` was called.
199 form : `NormalForm`
200 Form this expression is in. ``form.inner`` is the operator that
201 joins the operands in ``branches``.
203 Returns
204 -------
205 result
206 Implementation-defined result to be gathered and passed to
207 `visitOuter`.
208 """
209 raise NotImplementedError()
211 @abstractmethod
212 def visitOuter(self, branches: Sequence[_U], form: NormalForm) -> _V:
213 """Visit the sequence of outer AND (for `~NormalForm.CONJUNCTIVE` form)
214 or OR (for `~NormalForm.DISJUNCTIVE`) operands.
216 Parameters
217 ----------
218 branches : `~collections.abc.Sequence`
219 Sequence of return values from calls to `visitInner`.
220 form : `NormalForm`
221 Form this expression is in. ``form.outer`` is the operator that
222 joins the operands in ``branches``.
224 Returns
225 -------
226 result
227 Implementation-defined result to be returned by
228 `NormalFormExpression.visitNormalForm`.
229 """
230 raise NotImplementedError()
233class NormalFormExpression:
234 """A boolean expression in a standard normal form.
236 Most code should use `fromTree` to construct new instances instead of
237 calling the constructor directly. See `NormalForm` for a description of
238 the two forms.
240 Parameters
241 ----------
242 nodes : `~collections.abc.Sequence` [ `~collections.abc.Sequence` \
243 [ `Node` ] ]
244 Non-AND, non-OR branches of three tree, with the AND and OR operations
245 combining them represented by position in the nested sequence - the
246 inner sequence is combined via ``form.inner``, and the outer sequence
247 combines those via ``form.outer``.
248 form : `NormalForm`
249 Enumeration value indicating the form this expression is in.
250 """
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 return self._form
290 def visit(self, visitor: NormalFormVisitor[_T, _U, _V]) -> _V:
291 """Apply a visitor to the expression, explicitly taking advantage of
292 the special structure guaranteed by the normal forms.
294 Parameters
295 ----------
296 visitor : `NormalFormVisitor`
297 Visitor object to apply.
299 Returns
300 -------
301 result
302 Return value from calling ``visitor.visitOuter`` after visiting
303 all other nodes.
304 """
305 visitedOuterBranches: list[_U] = []
306 for nodeInnerBranches in self._nodes:
307 visitedInnerBranches = [visitor.visitBranch(node) for node in nodeInnerBranches]
308 visitedOuterBranches.append(visitor.visitInner(visitedInnerBranches, self.form))
309 return visitor.visitOuter(visitedOuterBranches, self.form)
311 def toTree(self) -> Node:
312 """Convert ``self`` back into an equivalent tree, preserving the
313 form of the boolean expression but representing it in memory as
314 a tree.
316 Returns
317 -------
318 tree : `Node`
319 Root of the tree equivalent to ``self``.
320 """
321 visitor = TreeReconstructionVisitor()
322 return self.visit(visitor)
325class PrecedenceTier(enum.Enum):
326 """An enumeration used to track operator precedence.
328 This enum is currently used only to inject parentheses into boolean
329 logic that has been manipulated into a new form, and because those
330 parentheses are only used for stringification, the goal here is human
331 readability, not precision.
333 Lower enum values represent tighter binding, but code deciding whether to
334 inject parentheses should always call `needsParens` instead of comparing
335 values directly.
336 """
338 TOKEN = 0
339 """Precedence tier for literals, identifiers, and expressions already in
340 parentheses.
341 """
343 UNARY = 1
344 """Precedence tier for unary operators, which always bind more tightly
345 than any binary operator.
346 """
348 VALUE_BINARY_OP = 2
349 """Precedence tier for binary operators that return non-boolean values.
350 """
352 COMPARISON = 3
353 """Precedence tier for binary comparison operators that accept non-boolean
354 values and return boolean values.
355 """
357 AND = 4
358 """Precedence tier for logical AND.
359 """
361 OR = 5
362 """Precedence tier for logical OR.
363 """
365 @classmethod
366 def needsParens(cls, outer: PrecedenceTier, inner: PrecedenceTier) -> bool:
367 """Test whether parentheses should be added around an operand.
369 Parameters
370 ----------
371 outer : `PrecedenceTier`
372 Precedence tier for the operation.
373 inner : `PrecedenceTier`
374 Precedence tier for the operand.
376 Returns
377 -------
378 needed : `bool`
379 If `True`, parentheses should be added.
381 Notes
382 -----
383 This method special cases logical binary operators for readability,
384 adding parentheses around ANDs embedded in ORs, and avoiding them in
385 chains of the same operator. If it is ever actually used with other
386 binary operators as ``outer``, those would probably merit similar
387 attention (or a totally new approach); its current approach would
388 aggressively add a lot of unfortunate parentheses because (aside from
389 this special-casing) it doesn't know about commutativity.
391 In fact, this method is rarely actually used for logical binary
392 operators either; the `LogicalBinaryOperation.unwrap` method that calls
393 it is never invoked by `NormalFormExpression` (the main public
394 interface), because those operators are flattened out (see
395 `TransformationWrapper.flatten`) instead. Parentheses are instead
396 added there by `TreeReconstructionVisitor`, which is simpler because
397 the structure of operators is restricted.
398 """
399 if outer is cls.OR and inner is cls.AND:
400 return True
401 if outer is cls.OR and inner is cls.OR:
402 return False
403 if outer is cls.AND and inner is cls.AND:
404 return False
405 return outer.value <= inner.value
408BINARY_OPERATOR_PRECEDENCE = {
409 "=": PrecedenceTier.COMPARISON,
410 "!=": PrecedenceTier.COMPARISON,
411 "<": PrecedenceTier.COMPARISON,
412 "<=": PrecedenceTier.COMPARISON,
413 ">": PrecedenceTier.COMPARISON,
414 ">=": PrecedenceTier.COMPARISON,
415 "OVERLAPS": PrecedenceTier.COMPARISON,
416 "+": PrecedenceTier.VALUE_BINARY_OP,
417 "-": PrecedenceTier.VALUE_BINARY_OP,
418 "*": PrecedenceTier.VALUE_BINARY_OP,
419 "/": PrecedenceTier.VALUE_BINARY_OP,
420 "AND": PrecedenceTier.AND,
421 "OR": PrecedenceTier.OR,
422}
425class TransformationWrapper(ABC):
426 """A base class for `Node` wrappers that can be used to transform boolean
427 operator expressions.
429 Notes
430 -----
431 `TransformationWrapper` instances should only be directly constructed by
432 each other or `TransformationVisitor`. No new subclasses beyond those in
433 this module should be added.
435 While `TransformationWrapper` and its subclasses contain the machinery
436 for transforming expressions to normal forms, it does not provide a
437 convenient interface for working with expressions in those forms; most code
438 should just use `NormalFormExpression` (which delegates to
439 `TransformationWrapper` internally) instead.
440 """
442 __slots__ = ()
444 @abstractmethod
445 def __str__(self) -> str:
446 raise NotImplementedError()
448 @property
449 @abstractmethod
450 def precedence(self) -> PrecedenceTier:
451 """Return the precedence tier for this node (`PrecedenceTier`).
453 Notes
454 -----
455 This is only used when reconstructing a full `Node` tree in `unwrap`
456 implementations, and only to inject parenthesis that are necessary for
457 correct stringification but nothing else (because the tree structure
458 itself embeds the correct order of operations already).
459 """
460 raise NotImplementedError()
462 @abstractmethod
463 def not_(self) -> TransformationWrapper:
464 """Return a wrapper that represents the logical NOT of ``self``.
466 Returns
467 -------
468 wrapper : `TransformationWrapper`
469 A wrapper that represents the logical NOT of ``self``.
470 """
471 raise NotImplementedError()
473 def satisfies(self, form: NormalForm) -> bool:
474 """Test whether this expressions is already in a normal form.
476 The default implementation is appropriate for "atomic" classes that
477 never contain any AND or OR operations at all.
479 Parameters
480 ----------
481 form : `NormalForm`
482 Enumeration indicating the form to test for.
484 Returns
485 -------
486 satisfies : `bool`
487 Whether ``self`` satisfies the requirements of ``form``.
488 """
489 return True
491 def normalize(self, form: NormalForm) -> TransformationWrapper:
492 """Return an expression equivalent to ``self`` in a normal form.
494 The default implementation is appropriate for "atomic" classes that
495 never contain any AND or OR operations at all.
497 Parameters
498 ----------
499 form : `NormalForm`
500 Enumeration indicating the form to convert to.
502 Returns
503 -------
504 normalized : `TransformationWrapper`
505 An expression equivalent to ``self`` for which
506 ``normalized.satisfies(form)`` returns `True`.
508 Notes
509 -----
510 Converting an arbitrary boolean expression to either normal form is an
511 NP-hard problem, and this is a brute-force algorithm. I'm not sure
512 what its actual algorithmic scaling is, but it'd definitely be a bad
513 idea to attempt to normalize an expression with hundreds or thousands
514 of clauses.
515 """
516 return self
518 def flatten(self, operator: LogicalBinaryOperator) -> Iterator[TransformationWrapper]:
519 """Recursively flatten the operands of any nested operators of the
520 given type.
522 For an expression like ``(A AND ((B OR C) AND D)`` (with
523 ``operator == AND``), `flatten` yields ``A, (B OR C), D``.
525 The default implementation is appropriate for "atomic" classes that
526 never contain any AND or OR operations at all.
528 Parameters
529 ----------
530 operator : `LogicalBinaryOperator`
531 Operator whose operands to flatten.
533 Yields
534 ------
535 operands : `~collections.abc.Iterator` [ `TransformationWrapper` ]
536 Operands that, if combined with ``operator``, yield an expression
537 equivalent to ``self``.
538 """
539 yield self
541 def _satisfiesDispatch(
542 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
543 ) -> bool:
544 """Test whether ``operator.apply(self, other)`` is in a normal form.
546 The default implementation is appropriate for classes that never
547 contain any AND or OR operations at all.
549 Parameters
550 ----------
551 operator : `LogicalBinaryOperator`
552 Operator for the operation being tested.
553 other : `TransformationWrapper`
554 Other operand for the operation being tested.
555 form : `NormalForm`
556 Normal form being tested for.
558 Returns
559 -------
560 satisfies : `bool`
561 Whether ``operator.apply(self, other)`` satisfies the requirements
562 of ``form``.
564 Notes
565 -----
566 Caller guarantees that ``self`` and ``other`` are already normalized.
567 """
568 return other._satisfiesDispatchAtomic(operator, self, form=form)
570 def _normalizeDispatch(
571 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
572 ) -> TransformationWrapper:
573 """Return an expression equivalent to ``operator.apply(self, other)``
574 in a normal form.
576 The default implementation is appropriate for classes that never
577 contain any AND or OR operations at all.
579 Parameters
580 ----------
581 operator : `LogicalBinaryOperator`
582 Operator for the operation being transformed.
583 other : `TransformationWrapper`
584 Other operand for the operation being transformed.
585 form : `NormalForm`
586 Normal form being transformed to.
588 Returns
589 -------
590 normalized : `TransformationWrapper`
591 An expression equivalent to ``operator.apply(self, other)``
592 for which ``normalized.satisfies(form)`` returns `True`.
594 Notes
595 -----
596 Caller guarantees that ``self`` and ``other`` are already normalized.
597 """
598 return other._normalizeDispatchAtomic(operator, self, form=form)
600 def _satisfiesDispatchAtomic(
601 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
602 ) -> bool:
603 """Test whather ``operator.apply(other, self)`` is in a normal form.
605 The default implementation is appropriate for "atomic" classes that
606 never contain any AND or OR operations at all.
608 Parameters
609 ----------
610 operator : `LogicalBinaryOperator`
611 Operator for the operation being tested.
612 other : `TransformationWrapper`
613 Other operand for the operation being tested.
614 form : `NormalForm`
615 Normal form being tested for.
617 Returns
618 -------
619 satisfies : `bool`
620 Whether ``operator.apply(other, self)`` satisfies the requirements
621 of ``form``.
623 Notes
624 -----
625 Should only be called by `_satisfiesDispatch` implementations; this
626 guarantees that ``other`` is atomic and ``self`` is normalized.
627 """
628 return True
630 def _normalizeDispatchAtomic(
631 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
632 ) -> TransformationWrapper:
633 """Return an expression equivalent to ``operator.apply(other, self)``,
634 in a normal form.
636 The default implementation is appropriate for "atomic" classes that
637 never contain any AND or OR operations at all.
639 Parameters
640 ----------
641 operator : `LogicalBinaryOperator`
642 Operator for the operation being transformed.
643 other : `TransformationWrapper`
644 Other operand for the operation being transformed.
645 form : `NormalForm`
646 Normal form being transformed to.
648 Returns
649 -------
650 normalized : `TransformationWrapper`
651 An expression equivalent to ``operator.apply(other, self)``
652 for which ``normalized.satisfies(form)`` returns `True`.
654 Notes
655 -----
656 Should only be called by `_normalizeDispatch` implementations; this
657 guarantees that ``other`` is atomic and ``self`` is normalized.
658 """
659 return operator.apply(other, self)
661 def _satisfiesDispatchBinary(
662 self,
663 outer: LogicalBinaryOperator,
664 lhs: TransformationWrapper,
665 inner: LogicalBinaryOperator,
666 rhs: TransformationWrapper,
667 *,
668 form: NormalForm,
669 ) -> bool:
670 """Test whether ``outer.apply(self, inner.apply(lhs, rhs))`` is in a
671 normal form.
673 The default implementation is appropriate for "atomic" classes that
674 never contain any AND or OR operations at all.
676 Parameters
677 ----------
678 outer : `LogicalBinaryOperator`
679 Outer operator for the expression being tested.
680 lhs : `TransformationWrapper`
681 One inner operand for the expression being tested.
682 inner : `LogicalBinaryOperator`
683 Inner operator for the expression being tested. This may or may
684 not be the same as ``outer``.
685 rhs : `TransformationWrapper`
686 The other inner operand for the expression being tested.
687 form : `NormalForm`
688 Normal form being transformed to.
690 Returns
691 -------
692 satisfies : `bool`
693 Whether ``outer.apply(self, inner.apply(lhs, rhs))`` satisfies the
694 requirements of ``form``.
696 Notes
697 -----
698 Should only be called by `_satisfiesDispatch` implementations; this
699 guarantees that ``self``, ``lhs``, and ``rhs`` are all normalized.
700 """
701 return form.allows(inner=inner, outer=outer)
703 def _normalizeDispatchBinary(
704 self,
705 outer: LogicalBinaryOperator,
706 lhs: TransformationWrapper,
707 inner: LogicalBinaryOperator,
708 rhs: TransformationWrapper,
709 *,
710 form: NormalForm,
711 ) -> TransformationWrapper:
712 """Return an expression equivalent to
713 ``outer.apply(self, inner.apply(lhs, rhs))``, meeting the guarantees of
714 `normalize`.
716 The default implementation is appropriate for "atomic" classes that
717 never contain any AND or OR operations at all.
719 Parameters
720 ----------
721 outer : `LogicalBinaryOperator`
722 Outer operator for the expression being transformed.
723 lhs : `TransformationWrapper`
724 One inner operand for the expression being transformed.
725 inner : `LogicalBinaryOperator`
726 Inner operator for the expression being transformed.
727 rhs : `TransformationWrapper`
728 The other inner operand for the expression being transformed.
729 form : `NormalForm`
730 Normal form being transformed to.
732 Returns
733 -------
734 normalized : `TransformationWrapper`
735 An expression equivalent to
736 ``outer.apply(self, inner.apply(lhs, rhs))`` for which
737 ``normalized.satisfies(form)`` returns `True`.
739 Notes
740 -----
741 Should only be called by `_normalizeDispatch` implementations; this
742 guarantees that ``self``, ``lhs``, and ``rhs`` are all normalized.
743 """
744 if form.allows(inner=inner, outer=outer):
745 return outer.apply(inner.apply(lhs, rhs), self)
746 else:
747 return inner.apply(
748 outer.apply(lhs, self).normalize(form),
749 outer.apply(rhs, self).normalize(form),
750 )
752 @abstractmethod
753 def unwrap(self) -> Node:
754 """Return a transformed expression tree.
756 Returns
757 -------
758 tree : `Node`
759 Tree node representing the same expression (and form) as ``self``.
760 """
761 raise NotImplementedError()
764class Opaque(TransformationWrapper):
765 """A `TransformationWrapper` implementation for tree nodes that do not need
766 to be modified in boolean expression transformations.
768 This includes all identifiers, literals, and operators whose arguments are
769 not boolean.
771 Parameters
772 ----------
773 node : `Node`
774 Node wrapped by ``self``.
775 precedence : `PrecedenceTier`
776 Enumeration indicating how tightly this node is bound.
777 """
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 always 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 """
819 def __init__(self, operand: Opaque):
820 self._operand = operand
822 __slots__ = ("_operand",)
824 def __str__(self) -> str:
825 return f"not({self._operand})"
827 @property
828 def precedence(self) -> PrecedenceTier:
829 # Docstring inherited from `TransformationWrapper`.
830 return PrecedenceTier.UNARY
832 def not_(self) -> TransformationWrapper:
833 # Docstring inherited from `TransformationWrapper`.
834 return self._operand
836 def unwrap(self) -> Node:
837 # Docstring inherited from `TransformationWrapper`.
838 node = self._operand.unwrap()
839 if PrecedenceTier.needsParens(self.precedence, self._operand.precedence):
840 node = Parens(node)
841 return UnaryOp("NOT", node)
844class LogicalBinaryOperation(TransformationWrapper):
845 """A `TransformationWrapper` implementation for logical OR and NOT
846 implementations.
848 Parameters
849 ----------
850 lhs : `TransformationWrapper`
851 First operand.
852 operator : `LogicalBinaryOperator`
853 Enumeration representing the operator.
854 rhs : `TransformationWrapper`
855 Second operand.
856 """
858 def __init__(
859 self, lhs: TransformationWrapper, operator: LogicalBinaryOperator, rhs: TransformationWrapper
860 ):
861 self._lhs = lhs
862 self._operator = operator
863 self._rhs = rhs
864 self._satisfiesCache: dict[NormalForm, bool] = {}
866 __slots__ = ("_lhs", "_operator", "_rhs", "_satisfiesCache")
868 def __str__(self) -> str:
869 return f"{self._operator.name.lower()}({self._lhs}, {self._rhs})"
871 @property
872 def precedence(self) -> PrecedenceTier:
873 # Docstring inherited from `TransformationWrapper`.
874 return BINARY_OPERATOR_PRECEDENCE[self._operator.name]
876 def not_(self) -> TransformationWrapper:
877 # Docstring inherited from `TransformationWrapper`.
878 return LogicalBinaryOperation(
879 self._lhs.not_(),
880 LogicalBinaryOperator(not self._operator.value),
881 self._rhs.not_(),
882 )
884 def satisfies(self, form: NormalForm) -> bool:
885 # Docstring inherited from `TransformationWrapper`.
886 r = self._satisfiesCache.get(form)
887 if r is None:
888 r = (
889 self._lhs.satisfies(form)
890 and self._rhs.satisfies(form)
891 and self._lhs._satisfiesDispatch(self._operator, self._rhs, form=form)
892 )
893 self._satisfiesCache[form] = r
894 return r
896 def normalize(self, form: NormalForm) -> TransformationWrapper:
897 # Docstring inherited from `TransformationWrapper`.
898 if self.satisfies(form):
899 return self
900 lhs = self._lhs.normalize(form)
901 rhs = self._rhs.normalize(form)
902 return lhs._normalizeDispatch(self._operator, rhs, form=form)
904 def flatten(self, operator: LogicalBinaryOperator) -> Iterator[TransformationWrapper]:
905 # Docstring inherited from `TransformationWrapper`.
906 if operator is self._operator:
907 yield from self._lhs.flatten(operator)
908 yield from self._rhs.flatten(operator)
909 else:
910 yield self
912 def _satisfiesDispatch(
913 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
914 ) -> bool:
915 # Docstring inherited from `TransformationWrapper`.
916 return other._satisfiesDispatchBinary(operator, self._lhs, self._operator, self._rhs, form=form)
918 def _normalizeDispatch(
919 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
920 ) -> TransformationWrapper:
921 # Docstring inherited from `TransformationWrapper`.
922 return other._normalizeDispatchBinary(operator, self._lhs, self._operator, self._rhs, form=form)
924 def _satisfiesDispatchAtomic(
925 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
926 ) -> bool:
927 # Docstring inherited from `TransformationWrapper`.
928 return form.allows(outer=operator, inner=self._operator)
930 def _normalizeDispatchAtomic(
931 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm
932 ) -> TransformationWrapper:
933 # Docstring inherited from `TransformationWrapper`.
934 # Normalizes an expression of the form:
935 #
936 # operator.apply(
937 # other,
938 # self._operator.apply(self._lhs, self._rhs),
939 # )
940 #
941 if form.allows(outer=operator, inner=self._operator):
942 return operator.apply(other, self)
943 else:
944 return self._operator.apply(
945 operator.apply(other, self._lhs).normalize(form),
946 operator.apply(other, self._rhs).normalize(form),
947 )
949 def _satisfiesDispatchBinary(
950 self,
951 outer: LogicalBinaryOperator,
952 lhs: TransformationWrapper,
953 inner: LogicalBinaryOperator,
954 rhs: TransformationWrapper,
955 *,
956 form: NormalForm,
957 ) -> bool:
958 # Docstring inherited from `TransformationWrapper`.
959 return form.allows(outer=outer, inner=inner) and form.allows(outer=outer, inner=self._operator)
961 def _normalizeDispatchBinary(
962 self,
963 outer: LogicalBinaryOperator,
964 lhs: TransformationWrapper,
965 inner: LogicalBinaryOperator,
966 rhs: TransformationWrapper,
967 *,
968 form: NormalForm,
969 ) -> TransformationWrapper:
970 # Docstring inherited from `TransformationWrapper`.
971 # Normalizes an expression of the form:
972 #
973 # outer.apply(
974 # inner.apply(lhs, rhs),
975 # self._operator.apply(self._lhs, self._rhs),
976 # )
977 #
978 if form.allows(inner=inner, outer=outer):
979 other = inner.apply(lhs, rhs)
980 if form.allows(inner=self._operator, outer=outer):
981 return outer.apply(other, self)
982 else:
983 return self._operator.apply(
984 outer.apply(other, self._lhs).normalize(form),
985 outer.apply(other, self._rhs).normalize(form),
986 )
987 else:
988 if form.allows(inner=self._operator, outer=outer):
989 return inner.apply(
990 outer.apply(lhs, self).normalize(form), outer.apply(rhs, self).normalize(form)
991 )
992 else:
993 assert form.allows(inner=inner, outer=self._operator)
994 return self._operator.apply(
995 inner.apply(
996 outer.apply(lhs, self._lhs).normalize(form),
997 outer.apply(lhs, self._rhs).normalize(form),
998 ),
999 inner.apply(
1000 outer.apply(rhs, self._lhs).normalize(form),
1001 outer.apply(rhs, self._rhs).normalize(form),
1002 ),
1003 )
1005 def unwrap(self) -> Node:
1006 # Docstring inherited from `TransformationWrapper`.
1007 lhsNode = self._lhs.unwrap()
1008 if PrecedenceTier.needsParens(self.precedence, self._lhs.precedence):
1009 lhsNode = Parens(lhsNode)
1010 rhsNode = self._rhs.unwrap()
1011 if PrecedenceTier.needsParens(self.precedence, self._rhs.precedence):
1012 rhsNode = Parens(rhsNode)
1013 return BinaryOp(lhsNode, self._operator.name, rhsNode)
1016class TransformationVisitor(TreeVisitor[TransformationWrapper]):
1017 """A `TreeVisitor` implementation that constructs a `TransformationWrapper`
1018 tree when applied to a `Node` tree.
1019 """
1021 def visitNumericLiteral(self, value: str, node: Node) -> TransformationWrapper:
1022 # Docstring inherited from TreeVisitor.visitNumericLiteral
1023 return Opaque(node, PrecedenceTier.TOKEN)
1025 def visitStringLiteral(self, value: str, node: Node) -> TransformationWrapper:
1026 # Docstring inherited from TreeVisitor.visitStringLiteral
1027 return Opaque(node, PrecedenceTier.TOKEN)
1029 def visitTimeLiteral(self, value: astropy.time.Time, node: Node) -> TransformationWrapper:
1030 # Docstring inherited from TreeVisitor.visitTimeLiteral
1031 return Opaque(node, PrecedenceTier.TOKEN)
1033 def visitRangeLiteral(
1034 self, start: int, stop: int, stride: int | None, node: Node
1035 ) -> TransformationWrapper:
1036 # Docstring inherited from TreeVisitor.visitRangeLiteral
1037 return Opaque(node, PrecedenceTier.TOKEN)
1039 def visitIdentifier(self, name: str, node: Node) -> TransformationWrapper:
1040 # Docstring inherited from TreeVisitor.visitIdentifier
1041 return Opaque(node, PrecedenceTier.TOKEN)
1043 def visitUnaryOp(
1044 self,
1045 operator: str,
1046 operand: TransformationWrapper,
1047 node: Node,
1048 ) -> TransformationWrapper:
1049 # Docstring inherited from TreeVisitor.visitUnaryOp
1050 if operator == "NOT":
1051 return operand.not_()
1052 else:
1053 return Opaque(node, PrecedenceTier.UNARY)
1055 def visitBinaryOp(
1056 self,
1057 operator: str,
1058 lhs: TransformationWrapper,
1059 rhs: TransformationWrapper,
1060 node: Node,
1061 ) -> TransformationWrapper:
1062 # Docstring inherited from TreeVisitor.visitBinaryOp
1063 logical = LogicalBinaryOperator.__members__.get(operator)
1064 if logical is not None:
1065 return LogicalBinaryOperation(lhs, logical, rhs)
1066 return Opaque(node, BINARY_OPERATOR_PRECEDENCE[operator])
1068 def visitIsIn(
1069 self,
1070 lhs: TransformationWrapper,
1071 values: list[TransformationWrapper],
1072 not_in: bool,
1073 node: Node,
1074 ) -> TransformationWrapper:
1075 # Docstring inherited from TreeVisitor.visitIsIn
1076 return Opaque(node, PrecedenceTier.COMPARISON)
1078 def visitParens(self, expression: TransformationWrapper, node: Node) -> TransformationWrapper:
1079 # Docstring inherited from TreeVisitor.visitParens
1080 return expression
1082 def visitTupleNode(self, items: tuple[TransformationWrapper, ...], node: Node) -> TransformationWrapper:
1083 # Docstring inherited from TreeVisitor.visitTupleNode
1084 return Opaque(node, PrecedenceTier.TOKEN)
1086 def visitPointNode(
1087 self, ra: TransformationWrapper, dec: TransformationWrapper, node: Node
1088 ) -> TransformationWrapper:
1089 # Docstring inherited from TreeVisitor.visitPointNode
1090 raise NotImplementedError("POINT() function is not supported yet")
1093class TreeReconstructionVisitor(NormalFormVisitor[Node, Node, Node]):
1094 """A `NormalFormVisitor` that reconstructs complete expression tree.
1096 Outside code should use `NormalFormExpression.toTree` (which delegates to
1097 this visitor) instead.
1098 """
1100 def visitBranch(self, node: Node) -> Node:
1101 # Docstring inherited from NormalFormVisitor.
1102 return node
1104 def _visitSequence(self, branches: Sequence[Node], operator: LogicalBinaryOperator) -> Node:
1105 """Implement common recursive implementation for `visitInner` and
1106 `visitOuter`.
1108 Parameters
1109 ----------
1110 branches : `~collections.abc.Sequence`
1111 Sequence of return values from calls to `visitBranch`, representing
1112 a visited set of operands combined in the expression by
1113 ``operator``.
1114 operator : `LogicalBinaryOperator`
1115 Operator that joins the elements of ``branches``.
1117 Returns
1118 -------
1119 result
1120 Result of the final call to ``visitBranch(node)``.
1121 node : `Node`
1122 Hierarchical expression tree equivalent to joining ``branches``
1123 with ``operator``.
1124 """
1125 first, *rest = branches
1126 if not rest:
1127 return first
1128 merged = self._visitSequence(rest, operator)
1129 node = BinaryOp(first, operator.name, merged)
1130 return self.visitBranch(node)
1132 def visitInner(self, branches: Sequence[Node], form: NormalForm) -> Node:
1133 # Docstring inherited from NormalFormVisitor.
1134 node = self._visitSequence(branches, form.inner)
1135 if len(branches) > 1:
1136 node = Parens(node)
1137 return node
1139 def visitOuter(self, branches: Sequence[Node], form: NormalForm) -> Node:
1140 # Docstring inherited from NormalFormVisitor.
1141 node = self._visitSequence(branches, form.outer)
1142 if isinstance(node, Parens):
1143 node = node.expr
1144 return node