Coverage for python/lsst/daf/butler/registry/queries/expressions/normalForm.py: 44%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

252 statements  

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

21 

22from __future__ import annotations 

23 

24__all__ = ( 

25 "NormalForm", 

26 "NormalFormExpression", 

27 "NormalFormVisitor", 

28) 

29 

30from abc import ABC, abstractmethod 

31import enum 

32from typing import ( 

33 Dict, 

34 Generic, 

35 Iterator, 

36 List, 

37 Optional, 

38 Sequence, 

39 Tuple, 

40 TypeVar, 

41) 

42 

43import astropy.time 

44 

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

46 

47 

48class LogicalBinaryOperator(enum.Enum): 

49 """Enumeration for logical binary operators. 

50 

51 These intentionally have the same names (including capitalization) as the 

52 string binary operators used in the parser itself. Boolean values are 

53 used to enable generic code that works on either operator (particularly 

54 ``LogicalBinaryOperator(not self)`` as a way to get the other operator). 

55 

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

57 by `LogicalBinaryOperator` and `NormalForm`. 

58 """ 

59 

60 AND = True 

61 OR = False 

62 

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

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

65 applied to the given operands. 

66 

67 This is simply syntactic sugar for the `LogicalBinaryOperation` 

68 constructor. 

69 

70 Parameters 

71 ---------- 

72 lhs: `TransformationWrapper` 

73 First operand. 

74 rhs: `TransformationWrapper` 

75 Second operand. 

76 

77 Returns 

78 ------- 

79 operation : `LogicalBinaryOperation` 

80 Object representing the operation. 

81 """ 

82 return LogicalBinaryOperation(lhs, self, rhs) 

83 

84 

85class NormalForm(enum.Enum): 

86 """Enumeration for boolean normal forms. 

87 

88 Both normal forms require all NOT operands to be moved "inside" any AND 

89 or OR operations (i.e. by applying DeMorgan's laws), with either all AND 

90 operations or all OR operations appearing outside the other (`CONJUNCTIVE`, 

91 and `DISJUNCTIVE`, respectively). 

92 

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

94 form, and for interoperability with `LogicalBinaryOperator`. 

95 

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

97 by `LogicalBinaryOperator` and `NormalForm`. 

98 """ 

99 

100 CONJUNCTIVE = True 

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

102 

103 For example, ``A AND (B OR C)`` is in conjunctive normal form, but 

104 ``A OR (B AND C)`` and ``A AND (B OR (C AND D))`` are not. 

105 """ 

106 

107 DISJUNCTIVE = False 

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

109 

110 For example, ``A OR (B AND C)`` is in disjunctive normal form, but 

111 ``A AND (B OR C)`` and ``A OR (B AND (C OR D))`` are not. 

112 """ 

113 

114 @property 

115 def inner(self) -> LogicalBinaryOperator: 

116 """The operator that is not permitted to contain operations of the 

117 other type in this form. 

118 

119 Note that this operation may still appear as the outermost operator in 

120 an expression in this form if there are no operations of the other 

121 type. 

122 """ 

123 return LogicalBinaryOperator(not self.value) 

124 

125 @property 

126 def outer(self) -> LogicalBinaryOperator: 

127 """The operator that is not permitted to be contained by operations of 

128 the other type in this form. 

129 

130 Note that an operation of this type is not necessarily the outermost 

131 operator in an expression in this form; the expression need not contain 

132 any operations of this type. 

133 """ 

134 return LogicalBinaryOperator(self.value) 

135 

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

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

138 

139 Parameters 

140 ---------- 

141 inner : `LogicalBinaryOperator` 

142 Inner operator in an expression. May be the same as ``outer``. 

143 outer : `LogicalBinaryOperator` 

144 Outer operator in an expression. May be the same as ``inner``. 

145 

146 Returns 

147 ------- 

148 allowed : `bool` 

149 Whether this operation relationship is allowed. 

150 """ 

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

152 

153 

154_T = TypeVar("_T") 

155_U = TypeVar("_U") 

156_V = TypeVar("_V") 

157 

158 

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

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

161 

162 A visitor implementation may inherit from both `NormalFormVisitor` and 

163 `TreeVisitor` and implement `visitBranch` as ``node.visit(self)`` to 

164 visit each node of the entire tree (or similarly with composition, etc). 

165 In this case, `TreeVisitor.visitBinaryOp` will never be called with a 

166 logical OR or AND operation, because these will all have been moved into 

167 calls to `visitInner` and `visitOuter` instead. 

168 

169 See also `NormalFormExpression.visit`. 

170 """ 

171 

172 @abstractmethod 

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

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

175 

176 Parameters 

177 ---------- 

178 node : `Node` 

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

180 operations. 

181 

182 Returns 

183 ------- 

184 result 

185 Implementation-defined result to be gathered and passed to 

186 `visitInner`. 

187 """ 

188 raise NotImplementedError() 

189 

190 @abstractmethod 

191 def visitInner(self, branches: Sequence[_T], form: NormalForm) -> _U: 

192 """Visit a sequence of inner OR (for `~NormalForm.CONJUNCTIVE` form) 

193 or AND (for `~NormalForm.DISJUNCTIVE`) operands. 

194 

195 Parameters 

196 ---------- 

197 branches : `Sequence` 

198 Sequence of tuples, where the first element in each tuple is the 

199 result of a call to `visitBranch`, and the second is the `Node` on 

200 which `visitBranch` was called. 

201 form : `NormalForm` 

202 Form this expression is in. ``form.inner`` is the operator that 

203 joins the operands in ``branches``. 

204 

205 Returns 

206 ------- 

207 result 

208 Implementation-defined result to be gathered and passed to 

209 `visitOuter`. 

210 """ 

211 raise NotImplementedError() 

212 

213 @abstractmethod 

214 def visitOuter(self, branches: Sequence[_U], form: NormalForm) -> _V: 

215 """Visit the sequence of outer AND (for `~NormalForm.CONJUNCTIVE` form) 

216 or OR (for `~NormalForm.DISJUNCTIVE`) operands. 

217 

218 Parameters 

219 ---------- 

220 branches : `Sequence` 

221 Sequence of return values from calls to `visitInner`. 

222 form : `NormalForm` 

223 Form this expression is in. ``form.outer`` is the operator that 

224 joins the operands in ``branches``. 

225 

226 Returns 

227 ------- 

228 result 

229 Implementation-defined result to be returned by 

230 `NormalFormExpression.visitNormalForm`. 

231 """ 

232 raise NotImplementedError() 

233 

234 

235class NormalFormExpression: 

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

237 

238 Most code should use `fromTree` to construct new instances instead of 

239 calling the constructor directly. See `NormalForm` for a description of 

240 the two forms. 

241 

242 Parameters 

243 ---------- 

244 nodes : `Sequence` [ `Sequence` [ `Node` ] ] 

245 Non-AND, non-OR branches of three tree, with the AND and OR operations 

246 combining them represented by position in the nested sequence - the 

247 inner sequence is combined via ``form.inner``, and the outer sequence 

248 combines those via ``form.outer``. 

249 form : `NormalForm` 

250 Enumeration value indicating the form this expression is in. 

251 """ 

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

253 self._form = form 

254 self._nodes = nodes 

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

289 return self._form 

290 

291 def visit(self, visitor: NormalFormVisitor[_T, _U, _V]) -> _V: 

292 """Apply a visitor to the expression, explicitly taking advantage of 

293 the special structure guaranteed by the normal forms. 

294 

295 Parameters 

296 ---------- 

297 visitor : `NormalFormVisitor` 

298 Visitor object to apply. 

299 

300 Returns 

301 ------- 

302 result 

303 Return value from calling ``visitor.visitOuter`` after visiting 

304 all other nodes. 

305 """ 

306 visitedOuterBranches: List[_U] = [] 

307 for nodeInnerBranches in self._nodes: 

308 visitedInnerBranches = [visitor.visitBranch(node) for node in nodeInnerBranches] 

309 visitedOuterBranches.append(visitor.visitInner(visitedInnerBranches, self.form)) 

310 return visitor.visitOuter(visitedOuterBranches, self.form) 

311 

312 def toTree(self) -> Node: 

313 """Convert ``self`` back into an equivalent tree, preserving the 

314 form of the boolean expression but representing it in memory as 

315 a tree. 

316 

317 Returns 

318 ------- 

319 tree : `Node` 

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

321 """ 

322 visitor = TreeReconstructionVisitor() 

323 return self.visit(visitor) 

324 

325 

326class PrecedenceTier(enum.Enum): 

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

328 

329 This enum is currently used only to inject parentheses into boolean 

330 logic that has been manipulated into a new form, and because those 

331 parentheses are only used for stringification, the goal here is human 

332 readability, not precision. 

333 

334 Lower enum values represent tighter binding, but code deciding whether to 

335 inject parentheses should always call `needsParens` instead of comparing 

336 values directly. 

337 """ 

338 

339 TOKEN = 0 

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

341 parentheses. 

342 """ 

343 

344 UNARY = 1 

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

346 than any binary operator. 

347 """ 

348 

349 VALUE_BINARY_OP = 2 

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

351 """ 

352 

353 COMPARISON = 3 

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

355 values and return boolean values. 

356 """ 

357 

358 AND = 4 

359 """Precedence tier for logical AND. 

360 """ 

361 

362 OR = 5 

363 """Precedence tier for logical OR. 

364 """ 

365 

366 @classmethod 

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

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

369 

370 Parameters 

371 ---------- 

372 outer : `PrecedenceTier` 

373 Precedence tier for the operation. 

374 inner : `PrecedenceTier` 

375 Precedence tier for the operand. 

376 

377 Returns 

378 ------- 

379 needed : `bool` 

380 If `True`, parentheses should be added. 

381 

382 Notes 

383 ----- 

384 This method special cases logical binary operators for readability, 

385 adding parentheses around ANDs embedded in ORs, and avoiding them in 

386 chains of the same operator. If it is ever actually used with other 

387 binary operators as ``outer``, those would probably merit similar 

388 attention (or a totally new approach); its current approach would 

389 aggressively add a lot of unfortunate parentheses because (aside from 

390 this special-casing) it doesn't know about commutativity. 

391 

392 In fact, this method is rarely actually used for logical binary 

393 operators either; the `LogicalBinaryOperation.unwrap` method that calls 

394 it is never invoked by `NormalFormExpression` (the main public 

395 interface), because those operators are flattened out (see 

396 `TransformationWrapper.flatten`) instead. Parentheses are instead 

397 added there by `TreeReconstructionVisitor`, which is simpler because 

398 the structure of operators is restricted. 

399 """ 

400 if outer is cls.OR and inner is cls.AND: 

401 return True 

402 if outer is cls.OR and inner is cls.OR: 

403 return False 

404 if outer is cls.AND and inner is cls.AND: 

405 return False 

406 return outer.value <= inner.value 

407 

408 

409BINARY_OPERATOR_PRECEDENCE = { 

410 "=": PrecedenceTier.COMPARISON, 

411 "!=": PrecedenceTier.COMPARISON, 

412 "<": PrecedenceTier.COMPARISON, 

413 "<=": PrecedenceTier.COMPARISON, 

414 ">": PrecedenceTier.COMPARISON, 

415 ">=": PrecedenceTier.COMPARISON, 

416 "OVERLAPS": PrecedenceTier.COMPARISON, 

417 "+": PrecedenceTier.VALUE_BINARY_OP, 

418 "-": PrecedenceTier.VALUE_BINARY_OP, 

419 "*": PrecedenceTier.VALUE_BINARY_OP, 

420 "/": PrecedenceTier.VALUE_BINARY_OP, 

421 "AND": PrecedenceTier.AND, 

422 "OR": PrecedenceTier.OR, 

423} 

424 

425 

426class TransformationWrapper(ABC): 

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

428 operator expressions. 

429 

430 Notes 

431 ----- 

432 `TransformationWrapper` instances should only be directly constructed by 

433 each other or `TransformationVisitor`. No new subclasses beyond those in 

434 this module should be added. 

435 

436 While `TransformationWrapper` and its subclasses contain the machinery 

437 for transforming expressions to normal forms, it does not provide a 

438 convenient interface for working with expressions in those forms; most code 

439 should just use `NormalFormExpression` (which delegates to 

440 `TransformationWrapper` internally) instead. 

441 """ 

442 

443 __slots__ = () 

444 

445 @abstractmethod 

446 def __str__(self) -> str: 

447 raise NotImplementedError() 

448 

449 @property 

450 @abstractmethod 

451 def precedence(self) -> PrecedenceTier: 

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

453 

454 Notes 

455 ----- 

456 This is only used when reconstructing a full `Node` tree in `unwrap` 

457 implementations, and only to inject parenthesis that are necessary for 

458 correct stringification but nothing else (because the tree structure 

459 itself embeds the correct order of operations already). 

460 """ 

461 raise NotImplementedError() 

462 

463 @abstractmethod 

464 def not_(self) -> TransformationWrapper: 

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

466 

467 Returns 

468 ------- 

469 wrapper : `TransformationWrapper` 

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

471 """ 

472 raise NotImplementedError() 

473 

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

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

476 

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

478 never contain any AND or OR operations at all. 

479 

480 Parameters 

481 ---------- 

482 form : `NormalForm` 

483 Enumeration indicating the form to test for. 

484 

485 Returns 

486 ------- 

487 satisfies : `bool` 

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

489 """ 

490 return True 

491 

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

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

494 

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

496 never contain any AND or OR operations at all. 

497 

498 Parameters 

499 ---------- 

500 form : `NormalForm` 

501 Enumeration indicating the form to convert to. 

502 

503 Returns 

504 ------- 

505 normalized : `TransformationWrapper` 

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

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

508 

509 Notes 

510 ----- 

511 Converting an arbitrary boolean expression to either normal form is an 

512 NP-hard problem, and this is a brute-force algorithm. I'm not sure 

513 what its actual algorithmic scaling is, but it'd definitely be a bad 

514 idea to attempt to normalize an expression with hundreds or thousands 

515 of clauses. 

516 """ 

517 return self 

518 

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

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

521 given type. 

522 

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

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

525 

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

527 never contain any AND or OR operations at all. 

528 

529 Parameters 

530 ---------- 

531 operator : `LogicalBinaryOperator` 

532 Operator whose operands to flatten. 

533 

534 Returns 

535 ------- 

536 operands : `Iterator` [ `TransformationWrapper` ] 

537 Operands that, if combined with ``operator``, yield an expression 

538 equivalent to ``self``. 

539 """ 

540 yield self 

541 

542 def _satisfiesDispatch( 

543 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm 

544 ) -> bool: 

545 """Test whether ``operator.apply(self, other)`` is in a normal form. 

546 

547 The default implementation is appropriate for classes that never 

548 contain any AND or OR operations at all. 

549 

550 Parameters 

551 ---------- 

552 operator : `LogicalBinaryOperator` 

553 Operator for the operation being tested. 

554 other : `TransformationWrapper` 

555 Other operand for the operation being tested. 

556 form : `NormalForm` 

557 Normal form being tested for. 

558 

559 Returns 

560 ------- 

561 satisfies : `bool` 

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

563 of ``form``. 

564 

565 Notes 

566 ----- 

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

568 """ 

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

570 

571 def _normalizeDispatch( 

572 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm 

573 ) -> TransformationWrapper: 

574 """Return an expression equivalent to ``operator.apply(self, other)`` 

575 in a normal form. 

576 

577 The default implementation is appropriate for classes that never 

578 contain any AND or OR operations at all. 

579 

580 Parameters 

581 ---------- 

582 operator : `LogicalBinaryOperator` 

583 Operator for the operation being transformed. 

584 other : `TransformationWrapper` 

585 Other operand for the operation being transformed. 

586 form : `NormalForm` 

587 Normal form being transformed to. 

588 

589 Returns 

590 ------- 

591 normalized : `TransformationWrapper` 

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

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

594 

595 Notes 

596 ----- 

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

598 """ 

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

600 

601 def _satisfiesDispatchAtomic( 

602 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm 

603 ) -> bool: 

604 """Test whather ``operator.apply(other, self)`` is in a normal form. 

605 

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

607 never contain any AND or OR operations at all. 

608 

609 Parameters 

610 ---------- 

611 operator : `LogicalBinaryOperator` 

612 Operator for the operation being tested. 

613 other : `TransformationWrapper` 

614 Other operand for the operation being tested. 

615 form : `NormalForm` 

616 Normal form being tested for. 

617 

618 Returns 

619 ------- 

620 satisfies : `bool` 

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

622 of ``form``. 

623 

624 Notes 

625 ----- 

626 Should only be called by `_satisfiesDispatch` implementations; this 

627 guarantees that ``other`` is atomic and ``self`` is normalized. 

628 """ 

629 return True 

630 

631 def _normalizeDispatchAtomic( 

632 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm 

633 ) -> TransformationWrapper: 

634 """Return an expression equivalent to ``operator.apply(other, self)``, 

635 in a normal form. 

636 

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

638 never contain any AND or OR operations at all. 

639 

640 Parameters 

641 ---------- 

642 operator : `LogicalBinaryOperator` 

643 Operator for the operation being transformed. 

644 other : `TransformationWrapper` 

645 Other operand for the operation being transformed. 

646 form : `NormalForm` 

647 Normal form being transformed to. 

648 

649 Returns 

650 ------- 

651 normalized : `TransformationWrapper` 

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

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

654 

655 Notes 

656 ----- 

657 Should only be called by `_normalizeDispatch` implementations; this 

658 guarantees that ``other`` is atomic and ``self`` is normalized. 

659 """ 

660 return operator.apply(other, self) 

661 

662 def _satisfiesDispatchBinary( 

663 self, 

664 outer: LogicalBinaryOperator, 

665 lhs: TransformationWrapper, 

666 inner: LogicalBinaryOperator, 

667 rhs: TransformationWrapper, 

668 *, 

669 form: NormalForm, 

670 ) -> bool: 

671 """Test whether ``outer.apply(self, inner.apply(lhs, rhs))`` is in a 

672 normal form. 

673 

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

675 never contain any AND or OR operations at all. 

676 

677 Parameters 

678 ---------- 

679 outer : `LogicalBinaryOperator` 

680 Outer operator for the expression being tested. 

681 lhs : `TransformationWrapper` 

682 One inner operand for the expression being tested. 

683 inner : `LogicalBinaryOperator` 

684 Inner operator for the expression being tested. This may or may 

685 not be the same as ``outer``. 

686 rhs: `TransformationWrapper` 

687 The other inner operand for the expression being tested. 

688 form : `NormalForm` 

689 Normal form being transformed to. 

690 

691 Returns 

692 ------- 

693 satisfies : `bool` 

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

695 requirements of ``form``. 

696 

697 Notes 

698 ----- 

699 Should only be called by `_satisfiesDispatch` implementations; this 

700 guarantees that ``self``, ``lhs``, and ``rhs`` are all normalized. 

701 """ 

702 return form.allows(inner=inner, outer=outer) 

703 

704 def _normalizeDispatchBinary( 

705 self, 

706 outer: LogicalBinaryOperator, 

707 lhs: TransformationWrapper, 

708 inner: LogicalBinaryOperator, 

709 rhs: TransformationWrapper, 

710 *, 

711 form: NormalForm, 

712 ) -> TransformationWrapper: 

713 """Return an expression equivalent to 

714 ``outer.apply(self, inner.apply(lhs, rhs))``, meeting the guarantees of 

715 `normalize`. 

716 

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

718 never contain any AND or OR operations at all. 

719 

720 Parameters 

721 ---------- 

722 outer : `LogicalBinaryOperator` 

723 Outer operator for the expression being transformed. 

724 lhs : `TransformationWrapper` 

725 One inner operand for the expression being transformed. 

726 inner : `LogicalBinaryOperator` 

727 Inner operator for the expression being transformed. 

728 rhs: `TransformationWrapper` 

729 The other inner operand for the expression being transformed. 

730 form : `NormalForm` 

731 Normal form being transformed to. 

732 

733 Returns 

734 ------- 

735 normalized : `TransformationWrapper` 

736 An expression equivalent to 

737 ``outer.apply(self, inner.apply(lhs, rhs))`` for which 

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

739 

740 Notes 

741 ----- 

742 Should only be called by `_normalizeDispatch` implementations; this 

743 guarantees that ``self``, ``lhs``, and ``rhs`` are all normalized. 

744 """ 

745 if form.allows(inner=inner, outer=outer): 

746 return outer.apply(inner.apply(lhs, rhs), self) 

747 else: 

748 return inner.apply( 

749 outer.apply(lhs, self).normalize(form), 

750 outer.apply(rhs, self).normalize(form), 

751 ) 

752 

753 @abstractmethod 

754 def unwrap(self) -> Node: 

755 """Return an transformed expression tree. 

756 

757 Return 

758 ------ 

759 tree : `Node` 

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

761 """ 

762 raise NotImplementedError() 

763 

764 

765class Opaque(TransformationWrapper): 

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

767 to be modified in boolean expression transformations. 

768 

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

770 not boolean. 

771 

772 Parameters 

773 ---------- 

774 node : `Node` 

775 Node wrapped by ``self``. 

776 precedence : `PrecedenceTier` 

777 Enumeration indicating how tightly this node is bound. 

778 """ 

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

780 self._node = node 

781 self._precedence = precedence 

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 def __init__(self, operand: Opaque): 

819 self._operand = operand 

820 

821 __slots__ = ("_operand",) 

822 

823 def __str__(self) -> str: 

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

825 

826 @property 

827 def precedence(self) -> PrecedenceTier: 

828 # Docstring inherited from `TransformationWrapper`. 

829 return PrecedenceTier.UNARY 

830 

831 def not_(self) -> TransformationWrapper: 

832 # Docstring inherited from `TransformationWrapper`. 

833 return self._operand 

834 

835 def unwrap(self) -> Node: 

836 # Docstring inherited from `TransformationWrapper`. 

837 node = self._operand.unwrap() 

838 if PrecedenceTier.needsParens(self.precedence, self._operand.precedence): 

839 node = Parens(node) 

840 return UnaryOp("NOT", node) 

841 

842 

843class LogicalBinaryOperation(TransformationWrapper): 

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

845 implementations. 

846 

847 Parameters 

848 ---------- 

849 lhs : `TransformationWrapper` 

850 First operand. 

851 operator : `LogicalBinaryOperator` 

852 Enumeration representing the operator. 

853 rhs : `TransformationWrapper` 

854 Second operand. 

855 """ 

856 def __init__( 

857 self, lhs: TransformationWrapper, operator: LogicalBinaryOperator, rhs: TransformationWrapper 

858 ): 

859 self._lhs = lhs 

860 self._operator = operator 

861 self._rhs = rhs 

862 self._satisfiesCache: Dict[NormalForm, bool] = {} 

863 

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

865 

866 def __str__(self) -> str: 

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

868 

869 @property 

870 def precedence(self) -> PrecedenceTier: 

871 # Docstring inherited from `TransformationWrapper`. 

872 return BINARY_OPERATOR_PRECEDENCE[self._operator.name] 

873 

874 def not_(self) -> TransformationWrapper: 

875 # Docstring inherited from `TransformationWrapper`. 

876 return LogicalBinaryOperation( 

877 self._lhs.not_(), 

878 LogicalBinaryOperator(not self._operator.value), 

879 self._rhs.not_(), 

880 ) 

881 

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

883 # Docstring inherited from `TransformationWrapper`. 

884 r = self._satisfiesCache.get(form) 

885 if r is None: 

886 r = ( 

887 self._lhs.satisfies(form) 

888 and self._rhs.satisfies(form) 

889 and self._lhs._satisfiesDispatch(self._operator, self._rhs, form=form) 

890 ) 

891 self._satisfiesCache[form] = r 

892 return r 

893 

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

895 # Docstring inherited from `TransformationWrapper`. 

896 if self.satisfies(form): 

897 return self 

898 lhs = self._lhs.normalize(form) 

899 rhs = self._rhs.normalize(form) 

900 return lhs._normalizeDispatch(self._operator, rhs, form=form) 

901 

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

903 # Docstring inherited from `TransformationWrapper`. 

904 if operator is self._operator: 

905 yield from self._lhs.flatten(operator) 

906 yield from self._rhs.flatten(operator) 

907 else: 

908 yield self 

909 

910 def _satisfiesDispatch( 

911 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm 

912 ) -> bool: 

913 # Docstring inherited from `TransformationWrapper`. 

914 return other._satisfiesDispatchBinary(operator, self._lhs, self._operator, self._rhs, form=form) 

915 

916 def _normalizeDispatch( 

917 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm 

918 ) -> TransformationWrapper: 

919 # Docstring inherited from `TransformationWrapper`. 

920 return other._normalizeDispatchBinary(operator, self._lhs, self._operator, self._rhs, form=form) 

921 

922 def _satisfiesDispatchAtomic( 

923 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm 

924 ) -> bool: 

925 # Docstring inherited from `TransformationWrapper`. 

926 return form.allows(outer=operator, inner=self._operator) 

927 

928 def _normalizeDispatchAtomic( 

929 self, operator: LogicalBinaryOperator, other: TransformationWrapper, *, form: NormalForm 

930 ) -> TransformationWrapper: 

931 # Docstring inherited from `TransformationWrapper`. 

932 # Normalizes an expression of the form: 

933 # 

934 # operator.apply( 

935 # other, 

936 # self._operator.apply(self._lhs, self._rhs), 

937 # ) 

938 # 

939 if form.allows(outer=operator, inner=self._operator): 

940 return operator.apply(other, self) 

941 else: 

942 return self._operator.apply( 

943 operator.apply(other, self._lhs).normalize(form), 

944 operator.apply(other, self._rhs).normalize(form), 

945 ) 

946 

947 def _satisfiesDispatchBinary( 

948 self, 

949 outer: LogicalBinaryOperator, 

950 lhs: TransformationWrapper, 

951 inner: LogicalBinaryOperator, 

952 rhs: TransformationWrapper, 

953 *, 

954 form: NormalForm, 

955 ) -> bool: 

956 # Docstring inherited from `TransformationWrapper`. 

957 return form.allows(outer=outer, inner=inner) and form.allows(outer=outer, inner=self._operator) 

958 

959 def _normalizeDispatchBinary( 

960 self, 

961 outer: LogicalBinaryOperator, 

962 lhs: TransformationWrapper, 

963 inner: LogicalBinaryOperator, 

964 rhs: TransformationWrapper, 

965 *, 

966 form: NormalForm, 

967 ) -> TransformationWrapper: 

968 # Docstring inherited from `TransformationWrapper`. 

969 # Normalizes an expression of the form: 

970 # 

971 # outer.apply( 

972 # inner.apply(lhs, rhs), 

973 # self._operator.apply(self._lhs, self._rhs), 

974 # ) 

975 # 

976 if form.allows(inner=inner, outer=outer): 

977 other = inner.apply(lhs, rhs) 

978 if form.allows(inner=self._operator, outer=outer): 

979 return outer.apply(other, self) 

980 else: 

981 return self._operator.apply( 

982 outer.apply(other, self._lhs).normalize(form), 

983 outer.apply(other, self._rhs).normalize(form), 

984 ) 

985 else: 

986 if form.allows(inner=self._operator, outer=outer): 

987 return inner.apply( 

988 outer.apply(lhs, self).normalize(form), outer.apply(rhs, self).normalize(form) 

989 ) 

990 else: 

991 assert form.allows(inner=inner, outer=self._operator) 

992 return self._operator.apply( 

993 inner.apply( 

994 outer.apply(lhs, self._lhs).normalize(form), 

995 outer.apply(lhs, self._rhs).normalize(form), 

996 ), 

997 inner.apply( 

998 outer.apply(rhs, self._lhs).normalize(form), 

999 outer.apply(rhs, self._rhs).normalize(form), 

1000 ), 

1001 ) 

1002 

1003 def unwrap(self) -> Node: 

1004 # Docstring inherited from `TransformationWrapper`. 

1005 lhsNode = self._lhs.unwrap() 

1006 if PrecedenceTier.needsParens(self.precedence, self._lhs.precedence): 

1007 lhsNode = Parens(lhsNode) 

1008 rhsNode = self._rhs.unwrap() 

1009 if PrecedenceTier.needsParens(self.precedence, self._rhs.precedence): 

1010 rhsNode = Parens(rhsNode) 

1011 return BinaryOp(lhsNode, self._operator.name, rhsNode) 

1012 

1013 

1014class TransformationVisitor(TreeVisitor[TransformationWrapper]): 

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

1016 tree when applied to a `Node` tree. 

1017 """ 

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

1019 # Docstring inherited from TreeVisitor.visitNumericLiteral 

1020 return Opaque(node, PrecedenceTier.TOKEN) 

1021 

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

1023 # Docstring inherited from TreeVisitor.visitStringLiteral 

1024 return Opaque(node, PrecedenceTier.TOKEN) 

1025 

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

1027 # Docstring inherited from TreeVisitor.visitTimeLiteral 

1028 return Opaque(node, PrecedenceTier.TOKEN) 

1029 

1030 def visitRangeLiteral( 

1031 self, start: int, stop: int, stride: Optional[int], node: Node 

1032 ) -> TransformationWrapper: 

1033 # Docstring inherited from TreeVisitor.visitRangeLiteral 

1034 return Opaque(node, PrecedenceTier.TOKEN) 

1035 

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

1037 # Docstring inherited from TreeVisitor.visitIdentifier 

1038 return Opaque(node, PrecedenceTier.TOKEN) 

1039 

1040 def visitUnaryOp( 

1041 self, 

1042 operator: str, 

1043 operand: TransformationWrapper, 

1044 node: Node, 

1045 ) -> TransformationWrapper: 

1046 # Docstring inherited from TreeVisitor.visitUnaryOp 

1047 if operator == "NOT": 

1048 return operand.not_() 

1049 else: 

1050 return Opaque(node, PrecedenceTier.UNARY) 

1051 

1052 def visitBinaryOp( 

1053 self, 

1054 operator: str, 

1055 lhs: TransformationWrapper, 

1056 rhs: TransformationWrapper, 

1057 node: Node, 

1058 ) -> TransformationWrapper: 

1059 # Docstring inherited from TreeVisitor.visitBinaryOp 

1060 logical = LogicalBinaryOperator.__members__.get(operator) 

1061 if logical is not None: 

1062 return LogicalBinaryOperation(lhs, logical, rhs) 

1063 return Opaque(node, BINARY_OPERATOR_PRECEDENCE[operator]) 

1064 

1065 def visitIsIn( 

1066 self, 

1067 lhs: TransformationWrapper, 

1068 values: List[TransformationWrapper], 

1069 not_in: bool, 

1070 node: Node, 

1071 ) -> TransformationWrapper: 

1072 # Docstring inherited from TreeVisitor.visitIsIn 

1073 return Opaque(node, PrecedenceTier.COMPARISON) 

1074 

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

1076 # Docstring inherited from TreeVisitor.visitParens 

1077 return expression 

1078 

1079 def visitTupleNode(self, items: Tuple[TransformationWrapper, ...], node: Node 

1080 ) -> TransformationWrapper: 

1081 # Docstring inherited from TreeVisitor.visitTupleNode 

1082 return Opaque(node, PrecedenceTier.TOKEN) 

1083 

1084 def visitPointNode(self, ra: TransformationWrapper, dec: TransformationWrapper, node: Node 

1085 ) -> TransformationWrapper: 

1086 # Docstring inherited from TreeVisitor.visitPointNode 

1087 raise NotImplementedError("POINT() function is not supported yet") 

1088 

1089 

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

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

1092 

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

1094 this visitor) instead. 

1095 """ 

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

1097 # Docstring inherited from NormalFormVisitor. 

1098 return node 

1099 

1100 def _visitSequence( 

1101 self, branches: Sequence[Node], operator: LogicalBinaryOperator 

1102 ) -> Node: 

1103 """Common recursive implementation for `visitInner` and `visitOuter`. 

1104 

1105 Parameters 

1106 ---------- 

1107 branches : `Sequence` 

1108 Sequence of return values from calls to `visitBranch`, representing 

1109 a visited set of operands combined in the expression by 

1110 ``operator``. 

1111 operator : `LogicalBinaryOperator` 

1112 Operator that joins the elements of ``branches``. 

1113 

1114 Returns 

1115 ------- 

1116 result 

1117 Result of the final call to ``visitBranch(node)``. 

1118 node : `Node` 

1119 Hierarchical expression tree equivalent to joining ``branches`` 

1120 with ``operator``. 

1121 """ 

1122 first, *rest = branches 

1123 if not rest: 

1124 return first 

1125 merged = self._visitSequence(rest, operator) 

1126 node = BinaryOp(first, operator.name, merged) 

1127 return self.visitBranch(node) 

1128 

1129 def visitInner(self, branches: Sequence[Node], form: NormalForm) -> Node: 

1130 # Docstring inherited from NormalFormVisitor. 

1131 node = self._visitSequence(branches, form.inner) 

1132 if len(branches) > 1: 

1133 node = Parens(node) 

1134 return node 

1135 

1136 def visitOuter(self, branches: Sequence[Node], form: NormalForm) -> Node: 

1137 # Docstring inherited from NormalFormVisitor. 

1138 node = self._visitSequence(branches, form.outer) 

1139 if isinstance(node, Parens): 

1140 node = node.expr 

1141 return node