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

260 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-16 02:54 -0800

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 

30import enum 

31from abc import ABC, abstractmethod 

32from typing import Dict, Generic, Iterator, List, Optional, Sequence, Tuple, TypeVar 

33 

34import astropy.time 

35 

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

37 

38 

39class LogicalBinaryOperator(enum.Enum): 

40 """Enumeration for logical binary operators. 

41 

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

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

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

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

46 

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

48 by `LogicalBinaryOperator` and `NormalForm`. 

49 """ 

50 

51 AND = True 

52 OR = False 

53 

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

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

56 applied to the given operands. 

57 

58 This is simply syntactic sugar for the `LogicalBinaryOperation` 

59 constructor. 

60 

61 Parameters 

62 ---------- 

63 lhs: `TransformationWrapper` 

64 First operand. 

65 rhs: `TransformationWrapper` 

66 Second operand. 

67 

68 Returns 

69 ------- 

70 operation : `LogicalBinaryOperation` 

71 Object representing the operation. 

72 """ 

73 return LogicalBinaryOperation(lhs, self, rhs) 

74 

75 

76class NormalForm(enum.Enum): 

77 """Enumeration for boolean normal forms. 

78 

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

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

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

82 and `DISJUNCTIVE`, respectively). 

83 

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

85 form, and for interoperability with `LogicalBinaryOperator`. 

86 

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

88 by `LogicalBinaryOperator` and `NormalForm`. 

89 """ 

90 

91 CONJUNCTIVE = True 

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

93 

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

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

96 """ 

97 

98 DISJUNCTIVE = False 

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

100 

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

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

103 """ 

104 

105 @property 

106 def inner(self) -> LogicalBinaryOperator: 

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

108 other type in this form. 

109 

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

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

112 type. 

113 """ 

114 return LogicalBinaryOperator(not self.value) 

115 

116 @property 

117 def outer(self) -> LogicalBinaryOperator: 

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

119 the other type in this form. 

120 

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

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

123 any operations of this type. 

124 """ 

125 return LogicalBinaryOperator(self.value) 

126 

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

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

129 

130 Parameters 

131 ---------- 

132 inner : `LogicalBinaryOperator` 

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

134 outer : `LogicalBinaryOperator` 

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

136 

137 Returns 

138 ------- 

139 allowed : `bool` 

140 Whether this operation relationship is allowed. 

141 """ 

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

143 

144 

145_T = TypeVar("_T") 

146_U = TypeVar("_U") 

147_V = TypeVar("_V") 

148 

149 

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

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

152 

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

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

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

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

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

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

159 

160 See also `NormalFormExpression.visit`. 

161 """ 

162 

163 @abstractmethod 

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

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

166 

167 Parameters 

168 ---------- 

169 node : `Node` 

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

171 operations. 

172 

173 Returns 

174 ------- 

175 result 

176 Implementation-defined result to be gathered and passed to 

177 `visitInner`. 

178 """ 

179 raise NotImplementedError() 

180 

181 @abstractmethod 

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

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

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

185 

186 Parameters 

187 ---------- 

188 branches : `Sequence` 

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

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

191 which `visitBranch` was called. 

192 form : `NormalForm` 

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

194 joins the operands in ``branches``. 

195 

196 Returns 

197 ------- 

198 result 

199 Implementation-defined result to be gathered and passed to 

200 `visitOuter`. 

201 """ 

202 raise NotImplementedError() 

203 

204 @abstractmethod 

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

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

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

208 

209 Parameters 

210 ---------- 

211 branches : `Sequence` 

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

213 form : `NormalForm` 

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

215 joins the operands in ``branches``. 

216 

217 Returns 

218 ------- 

219 result 

220 Implementation-defined result to be returned by 

221 `NormalFormExpression.visitNormalForm`. 

222 """ 

223 raise NotImplementedError() 

224 

225 

226class NormalFormExpression: 

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

228 

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

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

231 the two forms. 

232 

233 Parameters 

234 ---------- 

235 nodes : `Sequence` [ `Sequence` [ `Node` ] ] 

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

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

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

239 combines those via ``form.outer``. 

240 form : `NormalForm` 

241 Enumeration value indicating the form this expression is in. 

242 """ 

243 

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

245 self._form = form 

246 self._nodes = nodes 

247 

248 def __str__(self) -> str: 

249 return str(self.toTree()) 

250 

251 @staticmethod 

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

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

254 expression tree. 

255 

256 Parameters 

257 ---------- 

258 root : `Node` 

259 Root of the tree to be normalized. 

260 form : `NormalForm` 

261 Enumeration value indicating the form to normalize to. 

262 

263 Notes 

264 ----- 

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

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

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

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

269 of clauses. 

270 """ 

271 wrapper = root.visit(TransformationVisitor()).normalize(form) 

272 nodes = [] 

273 for outerOperands in wrapper.flatten(form.outer): 

274 nodes.append([w.unwrap() for w in outerOperands.flatten(form.inner)]) 

275 return NormalFormExpression(nodes, form=form) 

276 

277 @property 

278 def form(self) -> NormalForm: 

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

280 return self._form 

281 

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

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

284 the special structure guaranteed by the normal forms. 

285 

286 Parameters 

287 ---------- 

288 visitor : `NormalFormVisitor` 

289 Visitor object to apply. 

290 

291 Returns 

292 ------- 

293 result 

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

295 all other nodes. 

296 """ 

297 visitedOuterBranches: List[_U] = [] 

298 for nodeInnerBranches in self._nodes: 

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

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

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

302 

303 def toTree(self) -> Node: 

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

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

306 a tree. 

307 

308 Returns 

309 ------- 

310 tree : `Node` 

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

312 """ 

313 visitor = TreeReconstructionVisitor() 

314 return self.visit(visitor) 

315 

316 

317class PrecedenceTier(enum.Enum): 

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

319 

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

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

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

323 readability, not precision. 

324 

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

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

327 values directly. 

328 """ 

329 

330 TOKEN = 0 

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

332 parentheses. 

333 """ 

334 

335 UNARY = 1 

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

337 than any binary operator. 

338 """ 

339 

340 VALUE_BINARY_OP = 2 

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

342 """ 

343 

344 COMPARISON = 3 

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

346 values and return boolean values. 

347 """ 

348 

349 AND = 4 

350 """Precedence tier for logical AND. 

351 """ 

352 

353 OR = 5 

354 """Precedence tier for logical OR. 

355 """ 

356 

357 @classmethod 

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

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

360 

361 Parameters 

362 ---------- 

363 outer : `PrecedenceTier` 

364 Precedence tier for the operation. 

365 inner : `PrecedenceTier` 

366 Precedence tier for the operand. 

367 

368 Returns 

369 ------- 

370 needed : `bool` 

371 If `True`, parentheses should be added. 

372 

373 Notes 

374 ----- 

375 This method special cases logical binary operators for readability, 

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

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

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

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

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

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

382 

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

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

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

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

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

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

389 the structure of operators is restricted. 

390 """ 

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

392 return True 

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

394 return False 

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

396 return False 

397 return outer.value <= inner.value 

398 

399 

400BINARY_OPERATOR_PRECEDENCE = { 

401 "=": PrecedenceTier.COMPARISON, 

402 "!=": PrecedenceTier.COMPARISON, 

403 "<": PrecedenceTier.COMPARISON, 

404 "<=": PrecedenceTier.COMPARISON, 

405 ">": PrecedenceTier.COMPARISON, 

406 ">=": PrecedenceTier.COMPARISON, 

407 "OVERLAPS": PrecedenceTier.COMPARISON, 

408 "+": PrecedenceTier.VALUE_BINARY_OP, 

409 "-": PrecedenceTier.VALUE_BINARY_OP, 

410 "*": PrecedenceTier.VALUE_BINARY_OP, 

411 "/": PrecedenceTier.VALUE_BINARY_OP, 

412 "AND": PrecedenceTier.AND, 

413 "OR": PrecedenceTier.OR, 

414} 

415 

416 

417class TransformationWrapper(ABC): 

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

419 operator expressions. 

420 

421 Notes 

422 ----- 

423 `TransformationWrapper` instances should only be directly constructed by 

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

425 this module should be added. 

426 

427 While `TransformationWrapper` and its subclasses contain the machinery 

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

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

430 should just use `NormalFormExpression` (which delegates to 

431 `TransformationWrapper` internally) instead. 

432 """ 

433 

434 __slots__ = () 

435 

436 @abstractmethod 

437 def __str__(self) -> str: 

438 raise NotImplementedError() 

439 

440 @property 

441 @abstractmethod 

442 def precedence(self) -> PrecedenceTier: 

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

444 

445 Notes 

446 ----- 

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

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

449 correct stringification but nothing else (because the tree structure 

450 itself embeds the correct order of operations already). 

451 """ 

452 raise NotImplementedError() 

453 

454 @abstractmethod 

455 def not_(self) -> TransformationWrapper: 

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

457 

458 Returns 

459 ------- 

460 wrapper : `TransformationWrapper` 

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

462 """ 

463 raise NotImplementedError() 

464 

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

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

467 

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

469 never contain any AND or OR operations at all. 

470 

471 Parameters 

472 ---------- 

473 form : `NormalForm` 

474 Enumeration indicating the form to test for. 

475 

476 Returns 

477 ------- 

478 satisfies : `bool` 

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

480 """ 

481 return True 

482 

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

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

485 

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

487 never contain any AND or OR operations at all. 

488 

489 Parameters 

490 ---------- 

491 form : `NormalForm` 

492 Enumeration indicating the form to convert to. 

493 

494 Returns 

495 ------- 

496 normalized : `TransformationWrapper` 

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

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

499 

500 Notes 

501 ----- 

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

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

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

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

506 of clauses. 

507 """ 

508 return self 

509 

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

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

512 given type. 

513 

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

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

516 

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

518 never contain any AND or OR operations at all. 

519 

520 Parameters 

521 ---------- 

522 operator : `LogicalBinaryOperator` 

523 Operator whose operands to flatten. 

524 

525 Returns 

526 ------- 

527 operands : `Iterator` [ `TransformationWrapper` ] 

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

529 equivalent to ``self``. 

530 """ 

531 yield self 

532 

533 def _satisfiesDispatch( 

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

535 ) -> bool: 

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

537 

538 The default implementation is appropriate for classes that never 

539 contain any AND or OR operations at all. 

540 

541 Parameters 

542 ---------- 

543 operator : `LogicalBinaryOperator` 

544 Operator for the operation being tested. 

545 other : `TransformationWrapper` 

546 Other operand for the operation being tested. 

547 form : `NormalForm` 

548 Normal form being tested for. 

549 

550 Returns 

551 ------- 

552 satisfies : `bool` 

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

554 of ``form``. 

555 

556 Notes 

557 ----- 

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

559 """ 

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

561 

562 def _normalizeDispatch( 

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

564 ) -> TransformationWrapper: 

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

566 in a normal form. 

567 

568 The default implementation is appropriate for classes that never 

569 contain any AND or OR operations at all. 

570 

571 Parameters 

572 ---------- 

573 operator : `LogicalBinaryOperator` 

574 Operator for the operation being transformed. 

575 other : `TransformationWrapper` 

576 Other operand for the operation being transformed. 

577 form : `NormalForm` 

578 Normal form being transformed to. 

579 

580 Returns 

581 ------- 

582 normalized : `TransformationWrapper` 

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

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

585 

586 Notes 

587 ----- 

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

589 """ 

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

591 

592 def _satisfiesDispatchAtomic( 

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

594 ) -> bool: 

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

596 

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

598 never contain any AND or OR operations at all. 

599 

600 Parameters 

601 ---------- 

602 operator : `LogicalBinaryOperator` 

603 Operator for the operation being tested. 

604 other : `TransformationWrapper` 

605 Other operand for the operation being tested. 

606 form : `NormalForm` 

607 Normal form being tested for. 

608 

609 Returns 

610 ------- 

611 satisfies : `bool` 

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

613 of ``form``. 

614 

615 Notes 

616 ----- 

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

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

619 """ 

620 return True 

621 

622 def _normalizeDispatchAtomic( 

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

624 ) -> TransformationWrapper: 

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

626 in a normal form. 

627 

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

629 never contain any AND or OR operations at all. 

630 

631 Parameters 

632 ---------- 

633 operator : `LogicalBinaryOperator` 

634 Operator for the operation being transformed. 

635 other : `TransformationWrapper` 

636 Other operand for the operation being transformed. 

637 form : `NormalForm` 

638 Normal form being transformed to. 

639 

640 Returns 

641 ------- 

642 normalized : `TransformationWrapper` 

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

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

645 

646 Notes 

647 ----- 

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

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

650 """ 

651 return operator.apply(other, self) 

652 

653 def _satisfiesDispatchBinary( 

654 self, 

655 outer: LogicalBinaryOperator, 

656 lhs: TransformationWrapper, 

657 inner: LogicalBinaryOperator, 

658 rhs: TransformationWrapper, 

659 *, 

660 form: NormalForm, 

661 ) -> bool: 

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

663 normal form. 

664 

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

666 never contain any AND or OR operations at all. 

667 

668 Parameters 

669 ---------- 

670 outer : `LogicalBinaryOperator` 

671 Outer operator for the expression being tested. 

672 lhs : `TransformationWrapper` 

673 One inner operand for the expression being tested. 

674 inner : `LogicalBinaryOperator` 

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

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

677 rhs: `TransformationWrapper` 

678 The other inner operand for the expression being tested. 

679 form : `NormalForm` 

680 Normal form being transformed to. 

681 

682 Returns 

683 ------- 

684 satisfies : `bool` 

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

686 requirements of ``form``. 

687 

688 Notes 

689 ----- 

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

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

692 """ 

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

694 

695 def _normalizeDispatchBinary( 

696 self, 

697 outer: LogicalBinaryOperator, 

698 lhs: TransformationWrapper, 

699 inner: LogicalBinaryOperator, 

700 rhs: TransformationWrapper, 

701 *, 

702 form: NormalForm, 

703 ) -> TransformationWrapper: 

704 """Return an expression equivalent to 

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

706 `normalize`. 

707 

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

709 never contain any AND or OR operations at all. 

710 

711 Parameters 

712 ---------- 

713 outer : `LogicalBinaryOperator` 

714 Outer operator for the expression being transformed. 

715 lhs : `TransformationWrapper` 

716 One inner operand for the expression being transformed. 

717 inner : `LogicalBinaryOperator` 

718 Inner operator for the expression being transformed. 

719 rhs: `TransformationWrapper` 

720 The other inner operand for the expression being transformed. 

721 form : `NormalForm` 

722 Normal form being transformed to. 

723 

724 Returns 

725 ------- 

726 normalized : `TransformationWrapper` 

727 An expression equivalent to 

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

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

730 

731 Notes 

732 ----- 

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

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

735 """ 

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

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

738 else: 

739 return inner.apply( 

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

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

742 ) 

743 

744 @abstractmethod 

745 def unwrap(self) -> Node: 

746 """Return an transformed expression tree. 

747 

748 Return 

749 ------ 

750 tree : `Node` 

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

752 """ 

753 raise NotImplementedError() 

754 

755 

756class Opaque(TransformationWrapper): 

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

758 to be modified in boolean expression transformations. 

759 

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

761 not boolean. 

762 

763 Parameters 

764 ---------- 

765 node : `Node` 

766 Node wrapped by ``self``. 

767 precedence : `PrecedenceTier` 

768 Enumeration indicating how tightly this node is bound. 

769 """ 

770 

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

772 self._node = node 

773 self._precedence = precedence 

774 

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

776 

777 def __str__(self) -> str: 

778 return str(self._node) 

779 

780 @property 

781 def precedence(self) -> PrecedenceTier: 

782 # Docstring inherited from `TransformationWrapper`. 

783 return self._precedence 

784 

785 def not_(self) -> TransformationWrapper: 

786 # Docstring inherited from `TransformationWrapper`. 

787 return LogicalNot(self) 

788 

789 def unwrap(self) -> Node: 

790 # Docstring inherited from `TransformationWrapper`. 

791 return self._node 

792 

793 

794class LogicalNot(TransformationWrapper): 

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

796 

797 Parameters 

798 ---------- 

799 operand : `TransformationWrapper` 

800 Wrapper representing the operand of the NOT operation. 

801 

802 Notes 

803 ----- 

804 Instances should always be created by calling `not_` on an existing 

805 `TransformationWrapper` instead of calling `LogicalNot` directly. 

806 `LogicalNot` should only be called directly by `Opaque.not_`. This 

807 guarantees that double-negatives are simplified away and NOT operations 

808 are moved inside any OR and AND operations at construction. 

809 """ 

810 

811 def __init__(self, operand: Opaque): 

812 self._operand = operand 

813 

814 __slots__ = ("_operand",) 

815 

816 def __str__(self) -> str: 

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

818 

819 @property 

820 def precedence(self) -> PrecedenceTier: 

821 # Docstring inherited from `TransformationWrapper`. 

822 return PrecedenceTier.UNARY 

823 

824 def not_(self) -> TransformationWrapper: 

825 # Docstring inherited from `TransformationWrapper`. 

826 return self._operand 

827 

828 def unwrap(self) -> Node: 

829 # Docstring inherited from `TransformationWrapper`. 

830 node = self._operand.unwrap() 

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

832 node = Parens(node) 

833 return UnaryOp("NOT", node) 

834 

835 

836class LogicalBinaryOperation(TransformationWrapper): 

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

838 implementations. 

839 

840 Parameters 

841 ---------- 

842 lhs : `TransformationWrapper` 

843 First operand. 

844 operator : `LogicalBinaryOperator` 

845 Enumeration representing the operator. 

846 rhs : `TransformationWrapper` 

847 Second operand. 

848 """ 

849 

850 def __init__( 

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

852 ): 

853 self._lhs = lhs 

854 self._operator = operator 

855 self._rhs = rhs 

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

857 

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

859 

860 def __str__(self) -> str: 

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

862 

863 @property 

864 def precedence(self) -> PrecedenceTier: 

865 # Docstring inherited from `TransformationWrapper`. 

866 return BINARY_OPERATOR_PRECEDENCE[self._operator.name] 

867 

868 def not_(self) -> TransformationWrapper: 

869 # Docstring inherited from `TransformationWrapper`. 

870 return LogicalBinaryOperation( 

871 self._lhs.not_(), 

872 LogicalBinaryOperator(not self._operator.value), 

873 self._rhs.not_(), 

874 ) 

875 

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

877 # Docstring inherited from `TransformationWrapper`. 

878 r = self._satisfiesCache.get(form) 

879 if r is None: 

880 r = ( 

881 self._lhs.satisfies(form) 

882 and self._rhs.satisfies(form) 

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

884 ) 

885 self._satisfiesCache[form] = r 

886 return r 

887 

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

889 # Docstring inherited from `TransformationWrapper`. 

890 if self.satisfies(form): 

891 return self 

892 lhs = self._lhs.normalize(form) 

893 rhs = self._rhs.normalize(form) 

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

895 

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

897 # Docstring inherited from `TransformationWrapper`. 

898 if operator is self._operator: 

899 yield from self._lhs.flatten(operator) 

900 yield from self._rhs.flatten(operator) 

901 else: 

902 yield self 

903 

904 def _satisfiesDispatch( 

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

906 ) -> bool: 

907 # Docstring inherited from `TransformationWrapper`. 

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

909 

910 def _normalizeDispatch( 

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

912 ) -> TransformationWrapper: 

913 # Docstring inherited from `TransformationWrapper`. 

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

915 

916 def _satisfiesDispatchAtomic( 

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

918 ) -> bool: 

919 # Docstring inherited from `TransformationWrapper`. 

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

921 

922 def _normalizeDispatchAtomic( 

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

924 ) -> TransformationWrapper: 

925 # Docstring inherited from `TransformationWrapper`. 

926 # Normalizes an expression of the form: 

927 # 

928 # operator.apply( 

929 # other, 

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

931 # ) 

932 # 

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

934 return operator.apply(other, self) 

935 else: 

936 return self._operator.apply( 

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

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

939 ) 

940 

941 def _satisfiesDispatchBinary( 

942 self, 

943 outer: LogicalBinaryOperator, 

944 lhs: TransformationWrapper, 

945 inner: LogicalBinaryOperator, 

946 rhs: TransformationWrapper, 

947 *, 

948 form: NormalForm, 

949 ) -> bool: 

950 # Docstring inherited from `TransformationWrapper`. 

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

952 

953 def _normalizeDispatchBinary( 

954 self, 

955 outer: LogicalBinaryOperator, 

956 lhs: TransformationWrapper, 

957 inner: LogicalBinaryOperator, 

958 rhs: TransformationWrapper, 

959 *, 

960 form: NormalForm, 

961 ) -> TransformationWrapper: 

962 # Docstring inherited from `TransformationWrapper`. 

963 # Normalizes an expression of the form: 

964 # 

965 # outer.apply( 

966 # inner.apply(lhs, rhs), 

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

968 # ) 

969 # 

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

971 other = inner.apply(lhs, rhs) 

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

973 return outer.apply(other, self) 

974 else: 

975 return self._operator.apply( 

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

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

978 ) 

979 else: 

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

981 return inner.apply( 

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

983 ) 

984 else: 

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

986 return self._operator.apply( 

987 inner.apply( 

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

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

990 ), 

991 inner.apply( 

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

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

994 ), 

995 ) 

996 

997 def unwrap(self) -> Node: 

998 # Docstring inherited from `TransformationWrapper`. 

999 lhsNode = self._lhs.unwrap() 

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

1001 lhsNode = Parens(lhsNode) 

1002 rhsNode = self._rhs.unwrap() 

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

1004 rhsNode = Parens(rhsNode) 

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

1006 

1007 

1008class TransformationVisitor(TreeVisitor[TransformationWrapper]): 

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

1010 tree when applied to a `Node` tree. 

1011 """ 

1012 

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

1014 # Docstring inherited from TreeVisitor.visitNumericLiteral 

1015 return Opaque(node, PrecedenceTier.TOKEN) 

1016 

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

1018 # Docstring inherited from TreeVisitor.visitStringLiteral 

1019 return Opaque(node, PrecedenceTier.TOKEN) 

1020 

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

1022 # Docstring inherited from TreeVisitor.visitTimeLiteral 

1023 return Opaque(node, PrecedenceTier.TOKEN) 

1024 

1025 def visitRangeLiteral( 

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

1027 ) -> TransformationWrapper: 

1028 # Docstring inherited from TreeVisitor.visitRangeLiteral 

1029 return Opaque(node, PrecedenceTier.TOKEN) 

1030 

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

1032 # Docstring inherited from TreeVisitor.visitIdentifier 

1033 return Opaque(node, PrecedenceTier.TOKEN) 

1034 

1035 def visitUnaryOp( 

1036 self, 

1037 operator: str, 

1038 operand: TransformationWrapper, 

1039 node: Node, 

1040 ) -> TransformationWrapper: 

1041 # Docstring inherited from TreeVisitor.visitUnaryOp 

1042 if operator == "NOT": 

1043 return operand.not_() 

1044 else: 

1045 return Opaque(node, PrecedenceTier.UNARY) 

1046 

1047 def visitBinaryOp( 

1048 self, 

1049 operator: str, 

1050 lhs: TransformationWrapper, 

1051 rhs: TransformationWrapper, 

1052 node: Node, 

1053 ) -> TransformationWrapper: 

1054 # Docstring inherited from TreeVisitor.visitBinaryOp 

1055 logical = LogicalBinaryOperator.__members__.get(operator) 

1056 if logical is not None: 

1057 return LogicalBinaryOperation(lhs, logical, rhs) 

1058 return Opaque(node, BINARY_OPERATOR_PRECEDENCE[operator]) 

1059 

1060 def visitIsIn( 

1061 self, 

1062 lhs: TransformationWrapper, 

1063 values: List[TransformationWrapper], 

1064 not_in: bool, 

1065 node: Node, 

1066 ) -> TransformationWrapper: 

1067 # Docstring inherited from TreeVisitor.visitIsIn 

1068 return Opaque(node, PrecedenceTier.COMPARISON) 

1069 

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

1071 # Docstring inherited from TreeVisitor.visitParens 

1072 return expression 

1073 

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

1075 # Docstring inherited from TreeVisitor.visitTupleNode 

1076 return Opaque(node, PrecedenceTier.TOKEN) 

1077 

1078 def visitPointNode( 

1079 self, ra: TransformationWrapper, dec: TransformationWrapper, node: Node 

1080 ) -> TransformationWrapper: 

1081 # Docstring inherited from TreeVisitor.visitPointNode 

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

1083 

1084 

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

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

1087 

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

1089 this visitor) instead. 

1090 """ 

1091 

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

1093 # Docstring inherited from NormalFormVisitor. 

1094 return node 

1095 

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

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

1098 

1099 Parameters 

1100 ---------- 

1101 branches : `Sequence` 

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

1103 a visited set of operands combined in the expression by 

1104 ``operator``. 

1105 operator : `LogicalBinaryOperator` 

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

1107 

1108 Returns 

1109 ------- 

1110 result 

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

1112 node : `Node` 

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

1114 with ``operator``. 

1115 """ 

1116 first, *rest = branches 

1117 if not rest: 

1118 return first 

1119 merged = self._visitSequence(rest, operator) 

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

1121 return self.visitBranch(node) 

1122 

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

1124 # Docstring inherited from NormalFormVisitor. 

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

1126 if len(branches) > 1: 

1127 node = Parens(node) 

1128 return node 

1129 

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

1131 # Docstring inherited from NormalFormVisitor. 

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

1133 if isinstance(node, Parens): 

1134 node = node.expr 

1135 return node