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

253 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-21 09:55 +0000

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ( 

25 "NormalForm", 

26 "NormalFormExpression", 

27 "NormalFormVisitor", 

28) 

29 

30import enum 

31from abc import ABC, abstractmethod 

32from collections.abc import Iterator, Sequence 

33from typing import Generic, TypeVar 

34 

35import astropy.time 

36 

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

38 

39 

40class LogicalBinaryOperator(enum.Enum): 

41 """Enumeration for logical binary operators. 

42 

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

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

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

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

47 

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

49 by `LogicalBinaryOperator` and `NormalForm`. 

50 """ 

51 

52 AND = True 

53 OR = False 

54 

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

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

57 applied to the given operands. 

58 

59 This is simply syntactic sugar for the `LogicalBinaryOperation` 

60 constructor. 

61 

62 Parameters 

63 ---------- 

64 lhs: `TransformationWrapper` 

65 First operand. 

66 rhs: `TransformationWrapper` 

67 Second operand. 

68 

69 Returns 

70 ------- 

71 operation : `LogicalBinaryOperation` 

72 Object representing the operation. 

73 """ 

74 return LogicalBinaryOperation(lhs, self, rhs) 

75 

76 

77class NormalForm(enum.Enum): 

78 """Enumeration for boolean normal forms. 

79 

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

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

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

83 and `DISJUNCTIVE`, respectively). 

84 

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

86 form, and for interoperability with `LogicalBinaryOperator`. 

87 

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

89 by `LogicalBinaryOperator` and `NormalForm`. 

90 """ 

91 

92 CONJUNCTIVE = True 

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

94 

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

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

97 """ 

98 

99 DISJUNCTIVE = False 

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

101 

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

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

104 """ 

105 

106 @property 

107 def inner(self) -> LogicalBinaryOperator: 

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

109 other type in this form. 

110 

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

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

113 type. 

114 """ 

115 return LogicalBinaryOperator(not self.value) 

116 

117 @property 

118 def outer(self) -> LogicalBinaryOperator: 

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

120 the other type in this form. 

121 

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

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

124 any operations of this type. 

125 """ 

126 return LogicalBinaryOperator(self.value) 

127 

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

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

130 

131 Parameters 

132 ---------- 

133 inner : `LogicalBinaryOperator` 

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

135 outer : `LogicalBinaryOperator` 

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

137 

138 Returns 

139 ------- 

140 allowed : `bool` 

141 Whether this operation relationship is allowed. 

142 """ 

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

144 

145 

146_T = TypeVar("_T") 

147_U = TypeVar("_U") 

148_V = TypeVar("_V") 

149 

150 

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

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

153 

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

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

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

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

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

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

160 

161 See also `NormalFormExpression.visit`. 

162 """ 

163 

164 @abstractmethod 

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

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

167 

168 Parameters 

169 ---------- 

170 node : `Node` 

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

172 operations. 

173 

174 Returns 

175 ------- 

176 result 

177 Implementation-defined result to be gathered and passed to 

178 `visitInner`. 

179 """ 

180 raise NotImplementedError() 

181 

182 @abstractmethod 

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

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

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

186 

187 Parameters 

188 ---------- 

189 branches : `~collections.abc.Sequence` 

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

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

192 which `visitBranch` was called. 

193 form : `NormalForm` 

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

195 joins the operands in ``branches``. 

196 

197 Returns 

198 ------- 

199 result 

200 Implementation-defined result to be gathered and passed to 

201 `visitOuter`. 

202 """ 

203 raise NotImplementedError() 

204 

205 @abstractmethod 

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

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

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

209 

210 Parameters 

211 ---------- 

212 branches : `~collections.abc.Sequence` 

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

214 form : `NormalForm` 

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

216 joins the operands in ``branches``. 

217 

218 Returns 

219 ------- 

220 result 

221 Implementation-defined result to be returned by 

222 `NormalFormExpression.visitNormalForm`. 

223 """ 

224 raise NotImplementedError() 

225 

226 

227class NormalFormExpression: 

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

229 

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

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

232 the two forms. 

233 

234 Parameters 

235 ---------- 

236 nodes : `~collections.abc.Sequence` [ `~collections.abc.Sequence` \ 

237 [ `Node` ] ] 

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

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

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

241 combines those via ``form.outer``. 

242 form : `NormalForm` 

243 Enumeration value indicating the form this expression is in. 

244 """ 

245 

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

247 self._form = form 

248 self._nodes = nodes 

249 

250 def __str__(self) -> str: 

251 return str(self.toTree()) 

252 

253 @staticmethod 

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

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

256 expression tree. 

257 

258 Parameters 

259 ---------- 

260 root : `Node` 

261 Root of the tree to be normalized. 

262 form : `NormalForm` 

263 Enumeration value indicating the form to normalize to. 

264 

265 Notes 

266 ----- 

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

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

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

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

271 of clauses. 

272 """ 

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

274 nodes = [] 

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

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

277 return NormalFormExpression(nodes, form=form) 

278 

279 @property 

280 def form(self) -> NormalForm: 

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

282 return self._form 

283 

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

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

286 the special structure guaranteed by the normal forms. 

287 

288 Parameters 

289 ---------- 

290 visitor : `NormalFormVisitor` 

291 Visitor object to apply. 

292 

293 Returns 

294 ------- 

295 result 

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

297 all other nodes. 

298 """ 

299 visitedOuterBranches: list[_U] = [] 

300 for nodeInnerBranches in self._nodes: 

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

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

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

304 

305 def toTree(self) -> Node: 

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

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

308 a tree. 

309 

310 Returns 

311 ------- 

312 tree : `Node` 

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

314 """ 

315 visitor = TreeReconstructionVisitor() 

316 return self.visit(visitor) 

317 

318 

319class PrecedenceTier(enum.Enum): 

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

321 

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

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

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

325 readability, not precision. 

326 

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

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

329 values directly. 

330 """ 

331 

332 TOKEN = 0 

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

334 parentheses. 

335 """ 

336 

337 UNARY = 1 

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

339 than any binary operator. 

340 """ 

341 

342 VALUE_BINARY_OP = 2 

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

344 """ 

345 

346 COMPARISON = 3 

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

348 values and return boolean values. 

349 """ 

350 

351 AND = 4 

352 """Precedence tier for logical AND. 

353 """ 

354 

355 OR = 5 

356 """Precedence tier for logical OR. 

357 """ 

358 

359 @classmethod 

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

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

362 

363 Parameters 

364 ---------- 

365 outer : `PrecedenceTier` 

366 Precedence tier for the operation. 

367 inner : `PrecedenceTier` 

368 Precedence tier for the operand. 

369 

370 Returns 

371 ------- 

372 needed : `bool` 

373 If `True`, parentheses should be added. 

374 

375 Notes 

376 ----- 

377 This method special cases logical binary operators for readability, 

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

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

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

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

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

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

384 

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

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

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

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

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

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

391 the structure of operators is restricted. 

392 """ 

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

394 return True 

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

396 return False 

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

398 return False 

399 return outer.value <= inner.value 

400 

401 

402BINARY_OPERATOR_PRECEDENCE = { 

403 "=": PrecedenceTier.COMPARISON, 

404 "!=": PrecedenceTier.COMPARISON, 

405 "<": PrecedenceTier.COMPARISON, 

406 "<=": PrecedenceTier.COMPARISON, 

407 ">": PrecedenceTier.COMPARISON, 

408 ">=": PrecedenceTier.COMPARISON, 

409 "OVERLAPS": PrecedenceTier.COMPARISON, 

410 "+": PrecedenceTier.VALUE_BINARY_OP, 

411 "-": PrecedenceTier.VALUE_BINARY_OP, 

412 "*": PrecedenceTier.VALUE_BINARY_OP, 

413 "/": PrecedenceTier.VALUE_BINARY_OP, 

414 "AND": PrecedenceTier.AND, 

415 "OR": PrecedenceTier.OR, 

416} 

417 

418 

419class TransformationWrapper(ABC): 

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

421 operator expressions. 

422 

423 Notes 

424 ----- 

425 `TransformationWrapper` instances should only be directly constructed by 

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

427 this module should be added. 

428 

429 While `TransformationWrapper` and its subclasses contain the machinery 

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

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

432 should just use `NormalFormExpression` (which delegates to 

433 `TransformationWrapper` internally) instead. 

434 """ 

435 

436 __slots__ = () 

437 

438 @abstractmethod 

439 def __str__(self) -> str: 

440 raise NotImplementedError() 

441 

442 @property 

443 @abstractmethod 

444 def precedence(self) -> PrecedenceTier: 

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

446 

447 Notes 

448 ----- 

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

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

451 correct stringification but nothing else (because the tree structure 

452 itself embeds the correct order of operations already). 

453 """ 

454 raise NotImplementedError() 

455 

456 @abstractmethod 

457 def not_(self) -> TransformationWrapper: 

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

459 

460 Returns 

461 ------- 

462 wrapper : `TransformationWrapper` 

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

464 """ 

465 raise NotImplementedError() 

466 

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

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

469 

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

471 never contain any AND or OR operations at all. 

472 

473 Parameters 

474 ---------- 

475 form : `NormalForm` 

476 Enumeration indicating the form to test for. 

477 

478 Returns 

479 ------- 

480 satisfies : `bool` 

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

482 """ 

483 return True 

484 

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

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

487 

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

489 never contain any AND or OR operations at all. 

490 

491 Parameters 

492 ---------- 

493 form : `NormalForm` 

494 Enumeration indicating the form to convert to. 

495 

496 Returns 

497 ------- 

498 normalized : `TransformationWrapper` 

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

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

501 

502 Notes 

503 ----- 

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

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

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

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

508 of clauses. 

509 """ 

510 return self 

511 

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

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

514 given type. 

515 

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

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

518 

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

520 never contain any AND or OR operations at all. 

521 

522 Parameters 

523 ---------- 

524 operator : `LogicalBinaryOperator` 

525 Operator whose operands to flatten. 

526 

527 Returns 

528 ------- 

529 operands : `~collections.abc.Iterator` [ `TransformationWrapper` ] 

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

531 equivalent to ``self``. 

532 """ 

533 yield self 

534 

535 def _satisfiesDispatch( 

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

537 ) -> bool: 

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

539 

540 The default implementation is appropriate for classes that never 

541 contain any AND or OR operations at all. 

542 

543 Parameters 

544 ---------- 

545 operator : `LogicalBinaryOperator` 

546 Operator for the operation being tested. 

547 other : `TransformationWrapper` 

548 Other operand for the operation being tested. 

549 form : `NormalForm` 

550 Normal form being tested for. 

551 

552 Returns 

553 ------- 

554 satisfies : `bool` 

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

556 of ``form``. 

557 

558 Notes 

559 ----- 

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

561 """ 

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

563 

564 def _normalizeDispatch( 

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

566 ) -> TransformationWrapper: 

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

568 in a normal form. 

569 

570 The default implementation is appropriate for classes that never 

571 contain any AND or OR operations at all. 

572 

573 Parameters 

574 ---------- 

575 operator : `LogicalBinaryOperator` 

576 Operator for the operation being transformed. 

577 other : `TransformationWrapper` 

578 Other operand for the operation being transformed. 

579 form : `NormalForm` 

580 Normal form being transformed to. 

581 

582 Returns 

583 ------- 

584 normalized : `TransformationWrapper` 

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

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

587 

588 Notes 

589 ----- 

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

591 """ 

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

593 

594 def _satisfiesDispatchAtomic( 

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

596 ) -> bool: 

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

598 

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

600 never contain any AND or OR operations at all. 

601 

602 Parameters 

603 ---------- 

604 operator : `LogicalBinaryOperator` 

605 Operator for the operation being tested. 

606 other : `TransformationWrapper` 

607 Other operand for the operation being tested. 

608 form : `NormalForm` 

609 Normal form being tested for. 

610 

611 Returns 

612 ------- 

613 satisfies : `bool` 

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

615 of ``form``. 

616 

617 Notes 

618 ----- 

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

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

621 """ 

622 return True 

623 

624 def _normalizeDispatchAtomic( 

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

626 ) -> TransformationWrapper: 

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

628 in a normal form. 

629 

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

631 never contain any AND or OR operations at all. 

632 

633 Parameters 

634 ---------- 

635 operator : `LogicalBinaryOperator` 

636 Operator for the operation being transformed. 

637 other : `TransformationWrapper` 

638 Other operand for the operation being transformed. 

639 form : `NormalForm` 

640 Normal form being transformed to. 

641 

642 Returns 

643 ------- 

644 normalized : `TransformationWrapper` 

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

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

647 

648 Notes 

649 ----- 

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

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

652 """ 

653 return operator.apply(other, self) 

654 

655 def _satisfiesDispatchBinary( 

656 self, 

657 outer: LogicalBinaryOperator, 

658 lhs: TransformationWrapper, 

659 inner: LogicalBinaryOperator, 

660 rhs: TransformationWrapper, 

661 *, 

662 form: NormalForm, 

663 ) -> bool: 

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

665 normal form. 

666 

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

668 never contain any AND or OR operations at all. 

669 

670 Parameters 

671 ---------- 

672 outer : `LogicalBinaryOperator` 

673 Outer operator for the expression being tested. 

674 lhs : `TransformationWrapper` 

675 One inner operand for the expression being tested. 

676 inner : `LogicalBinaryOperator` 

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

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

679 rhs: `TransformationWrapper` 

680 The other inner operand for the expression being tested. 

681 form : `NormalForm` 

682 Normal form being transformed to. 

683 

684 Returns 

685 ------- 

686 satisfies : `bool` 

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

688 requirements of ``form``. 

689 

690 Notes 

691 ----- 

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

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

694 """ 

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

696 

697 def _normalizeDispatchBinary( 

698 self, 

699 outer: LogicalBinaryOperator, 

700 lhs: TransformationWrapper, 

701 inner: LogicalBinaryOperator, 

702 rhs: TransformationWrapper, 

703 *, 

704 form: NormalForm, 

705 ) -> TransformationWrapper: 

706 """Return an expression equivalent to 

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

708 `normalize`. 

709 

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

711 never contain any AND or OR operations at all. 

712 

713 Parameters 

714 ---------- 

715 outer : `LogicalBinaryOperator` 

716 Outer operator for the expression being transformed. 

717 lhs : `TransformationWrapper` 

718 One inner operand for the expression being transformed. 

719 inner : `LogicalBinaryOperator` 

720 Inner operator for the expression being transformed. 

721 rhs: `TransformationWrapper` 

722 The other inner operand for the expression being transformed. 

723 form : `NormalForm` 

724 Normal form being transformed to. 

725 

726 Returns 

727 ------- 

728 normalized : `TransformationWrapper` 

729 An expression equivalent to 

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

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

732 

733 Notes 

734 ----- 

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

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

737 """ 

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

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

740 else: 

741 return inner.apply( 

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

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

744 ) 

745 

746 @abstractmethod 

747 def unwrap(self) -> Node: 

748 """Return an transformed expression tree. 

749 

750 Return: 

751 ------ 

752 tree : `Node` 

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

754 """ 

755 raise NotImplementedError() 

756 

757 

758class Opaque(TransformationWrapper): 

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

760 to be modified in boolean expression transformations. 

761 

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

763 not boolean. 

764 

765 Parameters 

766 ---------- 

767 node : `Node` 

768 Node wrapped by ``self``. 

769 precedence : `PrecedenceTier` 

770 Enumeration indicating how tightly this node is bound. 

771 """ 

772 

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

774 self._node = node 

775 self._precedence = precedence 

776 

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

778 

779 def __str__(self) -> str: 

780 return str(self._node) 

781 

782 @property 

783 def precedence(self) -> PrecedenceTier: 

784 # Docstring inherited from `TransformationWrapper`. 

785 return self._precedence 

786 

787 def not_(self) -> TransformationWrapper: 

788 # Docstring inherited from `TransformationWrapper`. 

789 return LogicalNot(self) 

790 

791 def unwrap(self) -> Node: 

792 # Docstring inherited from `TransformationWrapper`. 

793 return self._node 

794 

795 

796class LogicalNot(TransformationWrapper): 

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

798 

799 Parameters 

800 ---------- 

801 operand : `TransformationWrapper` 

802 Wrapper representing the operand of the NOT operation. 

803 

804 Notes 

805 ----- 

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

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

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

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

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

811 """ 

812 

813 def __init__(self, operand: Opaque): 

814 self._operand = operand 

815 

816 __slots__ = ("_operand",) 

817 

818 def __str__(self) -> str: 

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

820 

821 @property 

822 def precedence(self) -> PrecedenceTier: 

823 # Docstring inherited from `TransformationWrapper`. 

824 return PrecedenceTier.UNARY 

825 

826 def not_(self) -> TransformationWrapper: 

827 # Docstring inherited from `TransformationWrapper`. 

828 return self._operand 

829 

830 def unwrap(self) -> Node: 

831 # Docstring inherited from `TransformationWrapper`. 

832 node = self._operand.unwrap() 

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

834 node = Parens(node) 

835 return UnaryOp("NOT", node) 

836 

837 

838class LogicalBinaryOperation(TransformationWrapper): 

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

840 implementations. 

841 

842 Parameters 

843 ---------- 

844 lhs : `TransformationWrapper` 

845 First operand. 

846 operator : `LogicalBinaryOperator` 

847 Enumeration representing the operator. 

848 rhs : `TransformationWrapper` 

849 Second operand. 

850 """ 

851 

852 def __init__( 

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

854 ): 

855 self._lhs = lhs 

856 self._operator = operator 

857 self._rhs = rhs 

858 self._satisfiesCache: dict[NormalForm, bool] = {} 

859 

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

861 

862 def __str__(self) -> str: 

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

864 

865 @property 

866 def precedence(self) -> PrecedenceTier: 

867 # Docstring inherited from `TransformationWrapper`. 

868 return BINARY_OPERATOR_PRECEDENCE[self._operator.name] 

869 

870 def not_(self) -> TransformationWrapper: 

871 # Docstring inherited from `TransformationWrapper`. 

872 return LogicalBinaryOperation( 

873 self._lhs.not_(), 

874 LogicalBinaryOperator(not self._operator.value), 

875 self._rhs.not_(), 

876 ) 

877 

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

879 # Docstring inherited from `TransformationWrapper`. 

880 r = self._satisfiesCache.get(form) 

881 if r is None: 

882 r = ( 

883 self._lhs.satisfies(form) 

884 and self._rhs.satisfies(form) 

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

886 ) 

887 self._satisfiesCache[form] = r 

888 return r 

889 

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

891 # Docstring inherited from `TransformationWrapper`. 

892 if self.satisfies(form): 

893 return self 

894 lhs = self._lhs.normalize(form) 

895 rhs = self._rhs.normalize(form) 

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

897 

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

899 # Docstring inherited from `TransformationWrapper`. 

900 if operator is self._operator: 

901 yield from self._lhs.flatten(operator) 

902 yield from self._rhs.flatten(operator) 

903 else: 

904 yield self 

905 

906 def _satisfiesDispatch( 

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

908 ) -> bool: 

909 # Docstring inherited from `TransformationWrapper`. 

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

911 

912 def _normalizeDispatch( 

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

914 ) -> TransformationWrapper: 

915 # Docstring inherited from `TransformationWrapper`. 

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

917 

918 def _satisfiesDispatchAtomic( 

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

920 ) -> bool: 

921 # Docstring inherited from `TransformationWrapper`. 

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

923 

924 def _normalizeDispatchAtomic( 

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

926 ) -> TransformationWrapper: 

927 # Docstring inherited from `TransformationWrapper`. 

928 # Normalizes an expression of the form: 

929 # 

930 # operator.apply( 

931 # other, 

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

933 # ) 

934 # 

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

936 return operator.apply(other, self) 

937 else: 

938 return self._operator.apply( 

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

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

941 ) 

942 

943 def _satisfiesDispatchBinary( 

944 self, 

945 outer: LogicalBinaryOperator, 

946 lhs: TransformationWrapper, 

947 inner: LogicalBinaryOperator, 

948 rhs: TransformationWrapper, 

949 *, 

950 form: NormalForm, 

951 ) -> bool: 

952 # Docstring inherited from `TransformationWrapper`. 

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

954 

955 def _normalizeDispatchBinary( 

956 self, 

957 outer: LogicalBinaryOperator, 

958 lhs: TransformationWrapper, 

959 inner: LogicalBinaryOperator, 

960 rhs: TransformationWrapper, 

961 *, 

962 form: NormalForm, 

963 ) -> TransformationWrapper: 

964 # Docstring inherited from `TransformationWrapper`. 

965 # Normalizes an expression of the form: 

966 # 

967 # outer.apply( 

968 # inner.apply(lhs, rhs), 

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

970 # ) 

971 # 

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

973 other = inner.apply(lhs, rhs) 

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

975 return outer.apply(other, self) 

976 else: 

977 return self._operator.apply( 

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

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

980 ) 

981 else: 

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

983 return inner.apply( 

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

985 ) 

986 else: 

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

988 return self._operator.apply( 

989 inner.apply( 

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

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

992 ), 

993 inner.apply( 

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

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

996 ), 

997 ) 

998 

999 def unwrap(self) -> Node: 

1000 # Docstring inherited from `TransformationWrapper`. 

1001 lhsNode = self._lhs.unwrap() 

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

1003 lhsNode = Parens(lhsNode) 

1004 rhsNode = self._rhs.unwrap() 

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

1006 rhsNode = Parens(rhsNode) 

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

1008 

1009 

1010class TransformationVisitor(TreeVisitor[TransformationWrapper]): 

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

1012 tree when applied to a `Node` tree. 

1013 """ 

1014 

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

1016 # Docstring inherited from TreeVisitor.visitNumericLiteral 

1017 return Opaque(node, PrecedenceTier.TOKEN) 

1018 

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

1020 # Docstring inherited from TreeVisitor.visitStringLiteral 

1021 return Opaque(node, PrecedenceTier.TOKEN) 

1022 

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

1024 # Docstring inherited from TreeVisitor.visitTimeLiteral 

1025 return Opaque(node, PrecedenceTier.TOKEN) 

1026 

1027 def visitRangeLiteral( 

1028 self, start: int, stop: int, stride: int | None, node: Node 

1029 ) -> TransformationWrapper: 

1030 # Docstring inherited from TreeVisitor.visitRangeLiteral 

1031 return Opaque(node, PrecedenceTier.TOKEN) 

1032 

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

1034 # Docstring inherited from TreeVisitor.visitIdentifier 

1035 return Opaque(node, PrecedenceTier.TOKEN) 

1036 

1037 def visitUnaryOp( 

1038 self, 

1039 operator: str, 

1040 operand: TransformationWrapper, 

1041 node: Node, 

1042 ) -> TransformationWrapper: 

1043 # Docstring inherited from TreeVisitor.visitUnaryOp 

1044 if operator == "NOT": 

1045 return operand.not_() 

1046 else: 

1047 return Opaque(node, PrecedenceTier.UNARY) 

1048 

1049 def visitBinaryOp( 

1050 self, 

1051 operator: str, 

1052 lhs: TransformationWrapper, 

1053 rhs: TransformationWrapper, 

1054 node: Node, 

1055 ) -> TransformationWrapper: 

1056 # Docstring inherited from TreeVisitor.visitBinaryOp 

1057 logical = LogicalBinaryOperator.__members__.get(operator) 

1058 if logical is not None: 

1059 return LogicalBinaryOperation(lhs, logical, rhs) 

1060 return Opaque(node, BINARY_OPERATOR_PRECEDENCE[operator]) 

1061 

1062 def visitIsIn( 

1063 self, 

1064 lhs: TransformationWrapper, 

1065 values: list[TransformationWrapper], 

1066 not_in: bool, 

1067 node: Node, 

1068 ) -> TransformationWrapper: 

1069 # Docstring inherited from TreeVisitor.visitIsIn 

1070 return Opaque(node, PrecedenceTier.COMPARISON) 

1071 

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

1073 # Docstring inherited from TreeVisitor.visitParens 

1074 return expression 

1075 

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

1077 # Docstring inherited from TreeVisitor.visitTupleNode 

1078 return Opaque(node, PrecedenceTier.TOKEN) 

1079 

1080 def visitPointNode( 

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

1082 ) -> TransformationWrapper: 

1083 # Docstring inherited from TreeVisitor.visitPointNode 

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

1085 

1086 

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

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

1089 

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

1091 this visitor) instead. 

1092 """ 

1093 

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

1095 # Docstring inherited from NormalFormVisitor. 

1096 return node 

1097 

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

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

1100 `visitOuter`. 

1101 

1102 Parameters 

1103 ---------- 

1104 branches : `~collections.abc.Sequence` 

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

1106 a visited set of operands combined in the expression by 

1107 ``operator``. 

1108 operator : `LogicalBinaryOperator` 

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

1110 

1111 Returns 

1112 ------- 

1113 result 

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

1115 node : `Node` 

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

1117 with ``operator``. 

1118 """ 

1119 first, *rest = branches 

1120 if not rest: 

1121 return first 

1122 merged = self._visitSequence(rest, operator) 

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

1124 return self.visitBranch(node) 

1125 

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

1127 # Docstring inherited from NormalFormVisitor. 

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

1129 if len(branches) > 1: 

1130 node = Parens(node) 

1131 return node 

1132 

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

1134 # Docstring inherited from NormalFormVisitor. 

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

1136 if isinstance(node, Parens): 

1137 node = node.expr 

1138 return node