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-08 02:51 -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/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ( 

31 "NormalForm", 

32 "NormalFormExpression", 

33 "NormalFormVisitor", 

34) 

35 

36import enum 

37from abc import ABC, abstractmethod 

38from collections.abc import Iterator, Sequence 

39from typing import Generic, TypeVar 

40 

41import astropy.time 

42 

43from .parser import BinaryOp, Node, Parens, TreeVisitor, UnaryOp 

44 

45 

46class LogicalBinaryOperator(enum.Enum): 

47 """Enumeration for logical binary operators. 

48 

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). 

53 

54 Which is `True` and which is `False` is just a convention, but one shared 

55 by `LogicalBinaryOperator` and `NormalForm`. 

56 """ 

57 

58 AND = True 

59 OR = False 

60 

61 def apply(self, lhs: TransformationWrapper, rhs: TransformationWrapper) -> LogicalBinaryOperation: 

62 """Return a `TransformationWrapper` object representing this operator 

63 applied to the given operands. 

64 

65 This is simply syntactic sugar for the `LogicalBinaryOperation` 

66 constructor. 

67 

68 Parameters 

69 ---------- 

70 lhs : `TransformationWrapper` 

71 First operand. 

72 rhs : `TransformationWrapper` 

73 Second operand. 

74 

75 Returns 

76 ------- 

77 operation : `LogicalBinaryOperation` 

78 Object representing the operation. 

79 """ 

80 return LogicalBinaryOperation(lhs, self, rhs) 

81 

82 

83class NormalForm(enum.Enum): 

84 """Enumeration for boolean normal forms. 

85 

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). 

90 

91 Boolean values are used here to enable generic code that works on either 

92 form, and for interoperability with `LogicalBinaryOperator`. 

93 

94 Which is `True` and which is `False` is just a convention, but one shared 

95 by `LogicalBinaryOperator` and `NormalForm`. 

96 """ 

97 

98 CONJUNCTIVE = True 

99 """Form in which AND operations may have OR operands, but not the reverse. 

100 

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 """ 

104 

105 DISJUNCTIVE = False 

106 """Form in which OR operations may have AND operands, but not the reverse. 

107 

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 """ 

111 

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. 

116 

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) 

122 

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. 

127 

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) 

133 

134 def allows(self, *, inner: LogicalBinaryOperator, outer: LogicalBinaryOperator) -> bool: 

135 """Test whether this form allows the given operator relationships. 

136 

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``. 

143 

144 Returns 

145 ------- 

146 allowed : `bool` 

147 Whether this operation relationship is allowed. 

148 """ 

149 return inner == outer or outer is self.outer 

150 

151 

152_T = TypeVar("_T") 

153_U = TypeVar("_U") 

154_V = TypeVar("_V") 

155 

156 

157class NormalFormVisitor(Generic[_T, _U, _V]): 

158 """A visitor interface for `NormalFormExpression`. 

159 

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. 

166 

167 See also `NormalFormExpression.visit`. 

168 """ 

169 

170 @abstractmethod 

171 def visitBranch(self, node: Node) -> _T: 

172 """Visit a regular `Node` and its child nodes. 

173 

174 Parameters 

175 ---------- 

176 node : `Node` 

177 A branch of the expression tree that contains no AND or OR 

178 operations. 

179 

180 Returns 

181 ------- 

182 result 

183 Implementation-defined result to be gathered and passed to 

184 `visitInner`. 

185 """ 

186 raise NotImplementedError() 

187 

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. 

192 

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``. 

202 

203 Returns 

204 ------- 

205 result 

206 Implementation-defined result to be gathered and passed to 

207 `visitOuter`. 

208 """ 

209 raise NotImplementedError() 

210 

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. 

215 

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``. 

223 

224 Returns 

225 ------- 

226 result 

227 Implementation-defined result to be returned by 

228 `NormalFormExpression.visitNormalForm`. 

229 """ 

230 raise NotImplementedError() 

231 

232 

233class NormalFormExpression: 

234 """A boolean expression in a standard normal form. 

235 

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. 

239 

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 """ 

251 

252 def __init__(self, nodes: Sequence[Sequence[Node]], form: NormalForm): 

253 self._form = form 

254 self._nodes = nodes 

255 

256 def __str__(self) -> str: 

257 return str(self.toTree()) 

258 

259 @staticmethod 

260 def fromTree(root: Node, form: NormalForm) -> NormalFormExpression: 

261 """Construct a `NormalFormExpression` by normalizing an arbitrary 

262 expression tree. 

263 

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. 

270 

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) 

284 

285 @property 

286 def form(self) -> NormalForm: 

287 """Enumeration value indicating the form this expression is in.""" 

288 return self._form 

289 

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. 

293 

294 Parameters 

295 ---------- 

296 visitor : `NormalFormVisitor` 

297 Visitor object to apply. 

298 

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) 

310 

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. 

315 

316 Returns 

317 ------- 

318 tree : `Node` 

319 Root of the tree equivalent to ``self``. 

320 """ 

321 visitor = TreeReconstructionVisitor() 

322 return self.visit(visitor) 

323 

324 

325class PrecedenceTier(enum.Enum): 

326 """An enumeration used to track operator precedence. 

327 

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. 

332 

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 """ 

337 

338 TOKEN = 0 

339 """Precedence tier for literals, identifiers, and expressions already in 

340 parentheses. 

341 """ 

342 

343 UNARY = 1 

344 """Precedence tier for unary operators, which always bind more tightly 

345 than any binary operator. 

346 """ 

347 

348 VALUE_BINARY_OP = 2 

349 """Precedence tier for binary operators that return non-boolean values. 

350 """ 

351 

352 COMPARISON = 3 

353 """Precedence tier for binary comparison operators that accept non-boolean 

354 values and return boolean values. 

355 """ 

356 

357 AND = 4 

358 """Precedence tier for logical AND. 

359 """ 

360 

361 OR = 5 

362 """Precedence tier for logical OR. 

363 """ 

364 

365 @classmethod 

366 def needsParens(cls, outer: PrecedenceTier, inner: PrecedenceTier) -> bool: 

367 """Test whether parentheses should be added around an operand. 

368 

369 Parameters 

370 ---------- 

371 outer : `PrecedenceTier` 

372 Precedence tier for the operation. 

373 inner : `PrecedenceTier` 

374 Precedence tier for the operand. 

375 

376 Returns 

377 ------- 

378 needed : `bool` 

379 If `True`, parentheses should be added. 

380 

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. 

390 

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 

406 

407 

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} 

423 

424 

425class TransformationWrapper(ABC): 

426 """A base class for `Node` wrappers that can be used to transform boolean 

427 operator expressions. 

428 

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. 

434 

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 """ 

441 

442 __slots__ = () 

443 

444 @abstractmethod 

445 def __str__(self) -> str: 

446 raise NotImplementedError() 

447 

448 @property 

449 @abstractmethod 

450 def precedence(self) -> PrecedenceTier: 

451 """Return the precedence tier for this node (`PrecedenceTier`). 

452 

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() 

461 

462 @abstractmethod 

463 def not_(self) -> TransformationWrapper: 

464 """Return a wrapper that represents the logical NOT of ``self``. 

465 

466 Returns 

467 ------- 

468 wrapper : `TransformationWrapper` 

469 A wrapper that represents the logical NOT of ``self``. 

470 """ 

471 raise NotImplementedError() 

472 

473 def satisfies(self, form: NormalForm) -> bool: 

474 """Test whether this expressions is already in a normal form. 

475 

476 The default implementation is appropriate for "atomic" classes that 

477 never contain any AND or OR operations at all. 

478 

479 Parameters 

480 ---------- 

481 form : `NormalForm` 

482 Enumeration indicating the form to test for. 

483 

484 Returns 

485 ------- 

486 satisfies : `bool` 

487 Whether ``self`` satisfies the requirements of ``form``. 

488 """ 

489 return True 

490 

491 def normalize(self, form: NormalForm) -> TransformationWrapper: 

492 """Return an expression equivalent to ``self`` in a normal form. 

493 

494 The default implementation is appropriate for "atomic" classes that 

495 never contain any AND or OR operations at all. 

496 

497 Parameters 

498 ---------- 

499 form : `NormalForm` 

500 Enumeration indicating the form to convert to. 

501 

502 Returns 

503 ------- 

504 normalized : `TransformationWrapper` 

505 An expression equivalent to ``self`` for which 

506 ``normalized.satisfies(form)`` returns `True`. 

507 

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 

517 

518 def flatten(self, operator: LogicalBinaryOperator) -> Iterator[TransformationWrapper]: 

519 """Recursively flatten the operands of any nested operators of the 

520 given type. 

521 

522 For an expression like ``(A AND ((B OR C) AND D)`` (with 

523 ``operator == AND``), `flatten` yields ``A, (B OR C), D``. 

524 

525 The default implementation is appropriate for "atomic" classes that 

526 never contain any AND or OR operations at all. 

527 

528 Parameters 

529 ---------- 

530 operator : `LogicalBinaryOperator` 

531 Operator whose operands to flatten. 

532 

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 

540 

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. 

545 

546 The default implementation is appropriate for classes that never 

547 contain any AND or OR operations at all. 

548 

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. 

557 

558 Returns 

559 ------- 

560 satisfies : `bool` 

561 Whether ``operator.apply(self, other)`` satisfies the requirements 

562 of ``form``. 

563 

564 Notes 

565 ----- 

566 Caller guarantees that ``self`` and ``other`` are already normalized. 

567 """ 

568 return other._satisfiesDispatchAtomic(operator, self, form=form) 

569 

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. 

575 

576 The default implementation is appropriate for classes that never 

577 contain any AND or OR operations at all. 

578 

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. 

587 

588 Returns 

589 ------- 

590 normalized : `TransformationWrapper` 

591 An expression equivalent to ``operator.apply(self, other)`` 

592 for which ``normalized.satisfies(form)`` returns `True`. 

593 

594 Notes 

595 ----- 

596 Caller guarantees that ``self`` and ``other`` are already normalized. 

597 """ 

598 return other._normalizeDispatchAtomic(operator, self, form=form) 

599 

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. 

604 

605 The default implementation is appropriate for "atomic" classes that 

606 never contain any AND or OR operations at all. 

607 

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. 

616 

617 Returns 

618 ------- 

619 satisfies : `bool` 

620 Whether ``operator.apply(other, self)`` satisfies the requirements 

621 of ``form``. 

622 

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 

629 

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. 

635 

636 The default implementation is appropriate for "atomic" classes that 

637 never contain any AND or OR operations at all. 

638 

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. 

647 

648 Returns 

649 ------- 

650 normalized : `TransformationWrapper` 

651 An expression equivalent to ``operator.apply(other, self)`` 

652 for which ``normalized.satisfies(form)`` returns `True`. 

653 

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) 

660 

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. 

672 

673 The default implementation is appropriate for "atomic" classes that 

674 never contain any AND or OR operations at all. 

675 

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. 

689 

690 Returns 

691 ------- 

692 satisfies : `bool` 

693 Whether ``outer.apply(self, inner.apply(lhs, rhs))`` satisfies the 

694 requirements of ``form``. 

695 

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) 

702 

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`. 

715 

716 The default implementation is appropriate for "atomic" classes that 

717 never contain any AND or OR operations at all. 

718 

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. 

731 

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`. 

738 

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 ) 

751 

752 @abstractmethod 

753 def unwrap(self) -> Node: 

754 """Return a transformed expression tree. 

755 

756 Returns 

757 ------- 

758 tree : `Node` 

759 Tree node representing the same expression (and form) as ``self``. 

760 """ 

761 raise NotImplementedError() 

762 

763 

764class Opaque(TransformationWrapper): 

765 """A `TransformationWrapper` implementation for tree nodes that do not need 

766 to be modified in boolean expression transformations. 

767 

768 This includes all identifiers, literals, and operators whose arguments are 

769 not boolean. 

770 

771 Parameters 

772 ---------- 

773 node : `Node` 

774 Node wrapped by ``self``. 

775 precedence : `PrecedenceTier` 

776 Enumeration indicating how tightly this node is bound. 

777 """ 

778 

779 def __init__(self, node: Node, precedence: PrecedenceTier): 

780 self._node = node 

781 self._precedence = precedence 

782 

783 __slots__ = ("_node", "_precedence") 

784 

785 def __str__(self) -> str: 

786 return str(self._node) 

787 

788 @property 

789 def precedence(self) -> PrecedenceTier: 

790 # Docstring inherited from `TransformationWrapper`. 

791 return self._precedence 

792 

793 def not_(self) -> TransformationWrapper: 

794 # Docstring inherited from `TransformationWrapper`. 

795 return LogicalNot(self) 

796 

797 def unwrap(self) -> Node: 

798 # Docstring inherited from `TransformationWrapper`. 

799 return self._node 

800 

801 

802class LogicalNot(TransformationWrapper): 

803 """A `TransformationWrapper` implementation for logical NOT operations. 

804 

805 Parameters 

806 ---------- 

807 operand : `TransformationWrapper` 

808 Wrapper representing the operand of the NOT operation. 

809 

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 """ 

818 

819 def __init__(self, operand: Opaque): 

820 self._operand = operand 

821 

822 __slots__ = ("_operand",) 

823 

824 def __str__(self) -> str: 

825 return f"not({self._operand})" 

826 

827 @property 

828 def precedence(self) -> PrecedenceTier: 

829 # Docstring inherited from `TransformationWrapper`. 

830 return PrecedenceTier.UNARY 

831 

832 def not_(self) -> TransformationWrapper: 

833 # Docstring inherited from `TransformationWrapper`. 

834 return self._operand 

835 

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) 

842 

843 

844class LogicalBinaryOperation(TransformationWrapper): 

845 """A `TransformationWrapper` implementation for logical OR and NOT 

846 implementations. 

847 

848 Parameters 

849 ---------- 

850 lhs : `TransformationWrapper` 

851 First operand. 

852 operator : `LogicalBinaryOperator` 

853 Enumeration representing the operator. 

854 rhs : `TransformationWrapper` 

855 Second operand. 

856 """ 

857 

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] = {} 

865 

866 __slots__ = ("_lhs", "_operator", "_rhs", "_satisfiesCache") 

867 

868 def __str__(self) -> str: 

869 return f"{self._operator.name.lower()}({self._lhs}, {self._rhs})" 

870 

871 @property 

872 def precedence(self) -> PrecedenceTier: 

873 # Docstring inherited from `TransformationWrapper`. 

874 return BINARY_OPERATOR_PRECEDENCE[self._operator.name] 

875 

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 ) 

883 

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 

895 

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) 

903 

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 

911 

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) 

917 

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) 

923 

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) 

929 

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 ) 

948 

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) 

960 

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 ) 

1004 

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) 

1014 

1015 

1016class TransformationVisitor(TreeVisitor[TransformationWrapper]): 

1017 """A `TreeVisitor` implementation that constructs a `TransformationWrapper` 

1018 tree when applied to a `Node` tree. 

1019 """ 

1020 

1021 def visitNumericLiteral(self, value: str, node: Node) -> TransformationWrapper: 

1022 # Docstring inherited from TreeVisitor.visitNumericLiteral 

1023 return Opaque(node, PrecedenceTier.TOKEN) 

1024 

1025 def visitStringLiteral(self, value: str, node: Node) -> TransformationWrapper: 

1026 # Docstring inherited from TreeVisitor.visitStringLiteral 

1027 return Opaque(node, PrecedenceTier.TOKEN) 

1028 

1029 def visitTimeLiteral(self, value: astropy.time.Time, node: Node) -> TransformationWrapper: 

1030 # Docstring inherited from TreeVisitor.visitTimeLiteral 

1031 return Opaque(node, PrecedenceTier.TOKEN) 

1032 

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) 

1038 

1039 def visitIdentifier(self, name: str, node: Node) -> TransformationWrapper: 

1040 # Docstring inherited from TreeVisitor.visitIdentifier 

1041 return Opaque(node, PrecedenceTier.TOKEN) 

1042 

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) 

1054 

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]) 

1067 

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) 

1077 

1078 def visitParens(self, expression: TransformationWrapper, node: Node) -> TransformationWrapper: 

1079 # Docstring inherited from TreeVisitor.visitParens 

1080 return expression 

1081 

1082 def visitTupleNode(self, items: tuple[TransformationWrapper, ...], node: Node) -> TransformationWrapper: 

1083 # Docstring inherited from TreeVisitor.visitTupleNode 

1084 return Opaque(node, PrecedenceTier.TOKEN) 

1085 

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") 

1091 

1092 

1093class TreeReconstructionVisitor(NormalFormVisitor[Node, Node, Node]): 

1094 """A `NormalFormVisitor` that reconstructs complete expression tree. 

1095 

1096 Outside code should use `NormalFormExpression.toTree` (which delegates to 

1097 this visitor) instead. 

1098 """ 

1099 

1100 def visitBranch(self, node: Node) -> Node: 

1101 # Docstring inherited from NormalFormVisitor. 

1102 return node 

1103 

1104 def _visitSequence(self, branches: Sequence[Node], operator: LogicalBinaryOperator) -> Node: 

1105 """Implement common recursive implementation for `visitInner` and 

1106 `visitOuter`. 

1107 

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``. 

1116 

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) 

1131 

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 

1138 

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