Coverage for datesat / enumeration_baseline.py: 53.2%

676 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-10 23:47 +0000

1""" 

2Enumeration baseline implementation for comparison with DateSAT. 

3 

4This module provides a baseline solver that enumerates all valid dates 

5in the allowed range [1900-03-01 to 2100-02-28] for date variables and 

6checks which assignments satisfy the constraints. 

7This is a brute-force approach that guarantees 

8finding a solution if one exists, but may be very slow. 

9""" 

10 

11from datetime import date, timedelta 

12from dateutil.relativedelta import relativedelta 

13from typing import Any, Dict, Optional, Union, List, Tuple 

14import time 

15from .core import Date, Period 

16import itertools 

17import builtins 

18 

19# Module-level wrapper functions for Z3 boolean operators that work with ConstraintWrapper objects 

20def _wrap_constraint_for_enumeration(constraint: Any) -> "ConstraintWrapper": 

21 """Wrap a constraint in ConstraintWrapper if it's not already wrapped.""" 

22 if isinstance(constraint, ConstraintWrapper): 

23 return constraint 

24 elif isinstance(constraint, bool): 

25 # Use default argument to capture value, not reference 

26 return ConstraintWrapper(lambda c=constraint: c) 

27 elif callable(constraint): 

28 return ConstraintWrapper(constraint) 

29 else: 

30 # Check if this is a variable object with get_value() method (EvalBoolVar, EvalIntVar, etc.) 

31 # These should be evaluated by calling get_value(), not by calling the object itself 

32 if hasattr(constraint, "get_value"): 

33 # Capture the object with a default argument and call get_value() 

34 return ConstraintWrapper(lambda obj=constraint: bool(obj.get_value())) 

35 # Try to convert to bool, capture value with default argument 

36 return ConstraintWrapper(lambda c=constraint: bool(c)) 

37 

38 

39def Or_enumeration(*args) -> "ConstraintWrapper": 

40 """Wrapper for Z3's Or() that works with enumeration baseline ConstraintWrapper objects.""" 

41 wrapped_args = [_wrap_constraint_for_enumeration(arg) for arg in args] 

42 # Create tuple and capture as default argument to prevent closure issues 

43 wrapped_args_tuple = tuple(wrapped_args) 

44 return ConstraintWrapper( 

45 func=lambda wrapped=wrapped_args_tuple: any(c.evaluate() for c in wrapped), 

46 or_constraints=list(wrapped_args_tuple), 

47 ) 

48 

49 

50def And_enumeration(*args) -> "ConstraintWrapper": 

51 """Wrapper for Z3's And() that works with enumeration baseline ConstraintWrapper objects.""" 

52 wrapped_args = [_wrap_constraint_for_enumeration(arg) for arg in args] 

53 # Create a tuple instead of list to ensure immutability and prevent accidental sharing 

54 wrapped_args_tuple = tuple(wrapped_args) 

55 return ConstraintWrapper( 

56 func=lambda wrapped=wrapped_args_tuple: all(c.evaluate() for c in wrapped) 

57 ) 

58 

59 

60def Not_enumeration(arg) -> "ConstraintWrapper": 

61 """Wrapper for Z3's Not() that works with enumeration baseline ConstraintWrapper objects.""" 

62 wrapped_arg = _wrap_constraint_for_enumeration(arg) 

63 # Capture as default argument to prevent closure issues 

64 return ConstraintWrapper(func=lambda wrapped=wrapped_arg: not wrapped.evaluate()) 

65 

66 

67def Implies_enumeration(antecedent, consequent) -> "ConstraintWrapper": 

68 """Wrapper for Z3's Implies() that works with enumeration baseline ConstraintWrapper objects.""" 

69 wrapped_antecedent = _wrap_constraint_for_enumeration(antecedent) 

70 wrapped_consequent = _wrap_constraint_for_enumeration(consequent) 

71 # Explicitly capture as default arguments to avoid closure issues 

72 return ConstraintWrapper( 

73 func=lambda ant=wrapped_antecedent, cons=wrapped_consequent: not ant.evaluate() 

74 or cons.evaluate() 

75 ) 

76 

77 

78class ConstraintWrapper: 

79 """Wrapper for deferred constraint evaluation.""" 

80 

81 def __init__( 

82 self, func, var_ref=None, concrete_value=None, rhs_ref=None, or_constraints=None 

83 ): 

84 self.func = func 

85 self.var_ref = var_ref 

86 self.concrete_value = concrete_value 

87 self.rhs_ref = rhs_ref # For equality constraints: x == expr, store expr here 

88 self.or_constraints = ( 

89 or_constraints # For OR constraints: list of ConstraintWrapper objects 

90 ) 

91 

92 def evaluate(self) -> bool: 

93 try: 

94 # If this is an OR constraint, evaluate any of the sub-constraints 

95 if self.or_constraints is not None: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true

96 return any(c.evaluate() for c in self.or_constraints) 

97 return bool(self.func()) 

98 except (ValueError, TypeError): 

99 return False 

100 

101 # ★ prevent accidental boolean usage like: if (x == y): ... 

102 def __bool__(self): 

103 raise TypeError( 

104 "Constraint objects are not truthy. Add them to the solver via add_constraint()." 

105 ) 

106 

107 

108class EnumerationComponentVar: 

109 """Symbolic date component variable (year, month, or day) for use inside Date() constructor.""" 

110 

111 def __init__(self, name: str, min_val: int, max_val: int, component_type: str): 

112 self.name = name 

113 self.min_val = min_val 

114 self.max_val = max_val 

115 self.component_type = component_type # 'year', 'month', or 'day' 

116 self._value = None 

117 

118 def set_value(self, val: int) -> None: 

119 if self.min_val <= val <= self.max_val: 119 ↛ 122line 119 didn't jump to line 122 because the condition on line 119 was always true

120 self._value = val 

121 else: 

122 raise ValueError( 

123 f"{self.component_type} value {val} out of range [{self.min_val}, {self.max_val}]" 

124 ) 

125 

126 def get_value(self) -> Optional[int]: 

127 return self._value 

128 

129 def clear_value(self) -> None: 

130 self._value = None 

131 

132 def __str__(self) -> str: 

133 return f"EnumerationComponentVar({self.name}, {self.component_type})" 

134 

135 # Arithmetic operations for expressions like year_var + 1 

136 def __add__(self, other: int) -> "EnumerationComponentExpr": 

137 if not isinstance(other, int): 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true

138 raise TypeError(f"Cannot add {type(other)} to EnumerationComponentVar") 

139 return EnumerationComponentExpr(self, "add", other) 

140 

141 def __sub__(self, other: int) -> "EnumerationComponentExpr": 

142 if not isinstance(other, int): 

143 raise TypeError( 

144 f"Cannot subtract {type(other)} from EnumerationComponentVar" 

145 ) 

146 return EnumerationComponentExpr(self, "sub", other) 

147 

148 def __mul__(self, other: int) -> "EnumerationComponentExpr": 

149 if not isinstance(other, int): 

150 raise TypeError(f"Cannot multiply {type(other)} by EnumerationComponentVar") 

151 return EnumerationComponentExpr(self, "mul", other) 

152 

153 def __mod__(self, other: int) -> "EnumerationComponentExpr": 

154 if not isinstance(other, int): 

155 raise TypeError( 

156 f"Cannot compute modulo with {type(other)} on EnumerationComponentVar" 

157 ) 

158 return EnumerationComponentExpr(self, "mod", other) 

159 

160 # Comparison operations for integer variables 

161 def _compare( 

162 self, 

163 op: str, 

164 other: Union[int, "EnumerationComponentVar", "EnumerationComponentExpr"], 

165 ) -> ConstraintWrapper: 

166 """Compare this component variable with an integer, another variable, or expression.""" 

167 

168 def compare(): 

169 lhs_val = self.get_value() 

170 if lhs_val is None: 

171 return False 

172 

173 # Handle EnumerationComponentVar 

174 if isinstance(other, EnumerationComponentVar): 

175 rhs_val = other.get_value() 

176 # Handle EnumerationComponentExpr 

177 elif isinstance(other, EnumerationComponentExpr): 

178 rhs_val = other.evaluate() 

179 # Handle int 

180 else: 

181 try: 

182 rhs_val = int(other) 

183 except (ValueError, TypeError): 

184 return False 

185 

186 if rhs_val is None: 

187 return False 

188 

189 try: 

190 return getattr(lhs_val, f"__{op}__")(rhs_val) 

191 except (ValueError, TypeError): 

192 return False 

193 

194 return ConstraintWrapper(compare) 

195 

196 def __eq__(self, other: Union[int, "EnumerationComponentVar", "EnumerationComponentExpr"]) -> ConstraintWrapper: # type: ignore[override] 

197 return self._compare("eq", other) 

198 

199 def __ne__(self, other: Union[int, "EnumerationComponentVar", "EnumerationComponentExpr"]) -> ConstraintWrapper: # type: ignore[override] 

200 return self._compare("ne", other) 

201 

202 def __lt__( 

203 self, other: Union[int, "EnumerationComponentVar", "EnumerationComponentExpr"] 

204 ) -> ConstraintWrapper: 

205 return self._compare("lt", other) 

206 

207 def __le__( 

208 self, other: Union[int, "EnumerationComponentVar", "EnumerationComponentExpr"] 

209 ) -> ConstraintWrapper: 

210 return self._compare("le", other) 

211 

212 def __gt__( 

213 self, other: Union[int, "EnumerationComponentVar", "EnumerationComponentExpr"] 

214 ) -> ConstraintWrapper: 

215 return self._compare("gt", other) 

216 

217 def __ge__( 

218 self, other: Union[int, "EnumerationComponentVar", "EnumerationComponentExpr"] 

219 ) -> ConstraintWrapper: 

220 return self._compare("ge", other) 

221 

222 

223class EnumerationComponentExpr: 

224 """Expression involving a component variable and a constant (e.g., year_var + 1, or x.year + 4).""" 

225 

226 def __init__(self, var: EnumerationComponentVar, op: str, constant: int): 

227 self.var = var 

228 self.op = op 

229 self.constant = constant 

230 

231 def evaluate(self) -> Optional[int]: 

232 # Check if this expression is based on a parent component (from a date variable) 

233 if hasattr(self.var, "_parent_component"): 

234 val = self.var._parent_component._get_component_value() 

235 else: 

236 val = self.var.get_value() 

237 

238 if val is None: 

239 return None 

240 if self.op == "add": 

241 return val + self.constant 

242 elif self.op == "sub": 

243 return val - self.constant 

244 elif self.op == "mul": 

245 return val * self.constant 

246 elif self.op == "mod": 

247 if self.constant == 0: 

248 return None # Division by zero 

249 return val % self.constant 

250 elif self.op == "floordiv": 

251 if self.constant == 0: 

252 return None # Division by zero 

253 return val // self.constant 

254 elif self.op == "sub_component": 

255 # Subtraction of another component: val - other_component_value 

256 # self.constant is not used, other_component is stored separately 

257 if hasattr(self, "_other_component"): 

258 other_val = self._other_component._get_component_value() 

259 if other_val is None: 

260 return None 

261 return val - other_val 

262 return None 

263 return None 

264 

265 def get_base_var(self) -> EnumerationComponentVar: 

266 return self.var 

267 

268 def _compare( 

269 self, 

270 op: str, 

271 other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"], 

272 ) -> ConstraintWrapper: 

273 """Compare this expression with an integer, date component, or another expression.""" 

274 

275 def compare(): 

276 expr_val = self.evaluate() 

277 if expr_val is None: 

278 return False 

279 

280 # Handle EnumerationDateComponent 

281 if isinstance(other, EnumerationDateComponent): 

282 other_val = other._get_component_value() 

283 # Handle EnumerationComponentExpr 

284 elif isinstance(other, EnumerationComponentExpr): 

285 other_val = other.evaluate() 

286 # Handle int 

287 else: 

288 try: 

289 other_val = int(other) 

290 except (ValueError, TypeError): 

291 return False 

292 

293 if other_val is None: 

294 return False 

295 

296 try: 

297 return getattr(expr_val, f"__{op}__")(other_val) 

298 except (ValueError, TypeError): 

299 return False 

300 

301 return ConstraintWrapper(compare) 

302 

303 def __eq__(self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"]) -> ConstraintWrapper: # type: ignore[override] 

304 return self._compare("eq", other) 

305 

306 def __ne__(self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"]) -> ConstraintWrapper: # type: ignore[override] 

307 return self._compare("ne", other) 

308 

309 def __lt__( 

310 self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"] 

311 ) -> ConstraintWrapper: 

312 return self._compare("lt", other) 

313 

314 def __le__( 

315 self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"] 

316 ) -> ConstraintWrapper: 

317 return self._compare("le", other) 

318 

319 def __gt__( 

320 self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"] 

321 ) -> ConstraintWrapper: 

322 return self._compare("gt", other) 

323 

324 def __ge__( 

325 self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"] 

326 ) -> ConstraintWrapper: 

327 return self._compare("ge", other) 

328 

329 

330class EnumerationDateVar: 

331 """Date variable for enumeration baseline that evaluates constraints concretely.""" 

332 

333 def __init__(self, name: str): 

334 self.name = name 

335 self._year = None 

336 self._month = None 

337 self._day = None 

338 self._value = None 

339 

340 def __str__(self) -> str: 

341 return f"EnumerationDateVar({self.name})" 

342 

343 def _hard_reset_value(self) -> None: 

344 """★ Reset cached concrete value for lazy nodes safely.""" 

345 self._value = None 

346 # do NOT blindly clear year/month/day for base variables; only lazy nodes call this 

347 

348 def set_value(self, year: int, month: int, day: int) -> None: 

349 self._year = year 

350 self._month = month 

351 self._day = day 

352 try: 

353 self._value = Date(year, month, day) 

354 except ValueError: 

355 self._value = None 

356 

357 def get_value(self) -> Optional[Date]: 

358 # Handle lazy operations 

359 if self._value is None and hasattr(self, "_lazy_op"): 

360 op_type, left, right = self._lazy_op 

361 # Evaluate left first (this recursively handles nested lazy operations) 

362 left_val = left.get_value() 

363 if left_val is None: 

364 return None 

365 # Apply the operation to the left value 

366 py_date = left_val.to_python_date() 

367 if op_type == "add": 

368 result_date = py_date + relativedelta( 

369 years=right.years, months=right.months, days=right.days 

370 ) 

371 elif op_type == "sub": 371 ↛ 379line 371 didn't jump to line 379 because the condition on line 371 was always true

372 neg_period = Period(-right.years, -right.months, -right.days) 

373 result_date = py_date + relativedelta( 

374 years=neg_period.years, 

375 months=neg_period.months, 

376 days=neg_period.days, 

377 ) 

378 else: # defensive 

379 return None 

380 try: 

381 result = Date.from_python_date(result_date) 

382 self.set_value(result.year, result.month, result.day) 

383 except ValueError: 

384 # Date outside allowed range - return None instead of raising 

385 # This allows the solver to properly return UNSAT 

386 return None 

387 return self._value 

388 

389 def to_concrete_date(self) -> Date: 

390 if self._value is None: 

391 raise ValueError(f"Date variable {self.name} has no concrete value") 

392 return self._value 

393 

394 def _get_comparison_func(self, op: str, other: Union[Date, "EnumerationDateVar"]): 

395 if isinstance(other, Date): 

396 other_date = other 

397 

398 def compare(): 

399 self_val = self.get_value() 

400 if self_val is None: 400 ↛ 401line 400 didn't jump to line 401 because the condition on line 400 was never true

401 return False 

402 return getattr(self_val.to_python_date(), f"__{op}__")( 

403 other_date.to_python_date() 

404 ) 

405 

406 return compare 

407 elif isinstance(other, EnumerationDateVar): 407 ↛ 421line 407 didn't jump to line 421 because the condition on line 407 was always true

408 other_ref = other 

409 

410 def compare(): 

411 self_val = self.get_value() 

412 other_val = other_ref.get_value() 

413 if self_val is None or other_val is None: 413 ↛ 414line 413 didn't jump to line 414 because the condition on line 413 was never true

414 return False 

415 return getattr(self_val.to_python_date(), f"__{op}__")( 

416 other_val.to_python_date() 

417 ) 

418 

419 return compare 

420 else: 

421 raise TypeError(f"Cannot compare EnumerationDateVar with {type(other)}") 

422 

423 def _get_equality_binding( 

424 self, other: Union[Date, "EnumerationDateVar"] 

425 ) -> Optional[Date]: 

426 if isinstance(other, Date): 

427 return other 

428 elif isinstance(other, EnumerationDateVar): 428 ↛ 430line 428 didn't jump to line 430 because the condition on line 428 was always true

429 return other.get_value() # may be None now; will be propagated later 

430 return None 

431 

432 def __ge__(self, other: Union[Date, "EnumerationDateVar"]) -> ConstraintWrapper: 

433 return ConstraintWrapper( 

434 self._get_comparison_func("ge", other), var_ref=self, rhs_ref=other 

435 ) 

436 

437 def __le__(self, other: Union[Date, "EnumerationDateVar"]) -> ConstraintWrapper: 

438 return ConstraintWrapper( 

439 self._get_comparison_func("le", other), var_ref=self, rhs_ref=other 

440 ) 

441 

442 def __lt__(self, other: Union[Date, "EnumerationDateVar"]) -> ConstraintWrapper: 

443 return ConstraintWrapper( 

444 self._get_comparison_func("lt", other), var_ref=self, rhs_ref=other 

445 ) 

446 

447 def __gt__(self, other: Union[Date, "EnumerationDateVar"]) -> ConstraintWrapper: 

448 return ConstraintWrapper( 

449 self._get_comparison_func("gt", other), var_ref=self, rhs_ref=other 

450 ) 

451 

452 def __eq__(self, other: Union[Date, "EnumerationDateVar"]) -> ConstraintWrapper: # type: ignore[override] 

453 concrete_value = self._get_equality_binding(other) 

454 return ConstraintWrapper( 

455 self._get_comparison_func("eq", other), 

456 var_ref=self, 

457 concrete_value=concrete_value, 

458 rhs_ref=other, 

459 ) 

460 

461 def __ne__(self, other: Union[Date, "EnumerationDateVar"]) -> ConstraintWrapper: # type: ignore[override] 

462 return ConstraintWrapper( 

463 self._get_comparison_func("ne", other), var_ref=self, rhs_ref=other 

464 ) 

465 

466 def __add__(self, other: Period) -> "EnumerationDateVar": 

467 if not isinstance(other, Period): 467 ↛ 468line 467 didn't jump to line 468 because the condition on line 467 was never true

468 raise TypeError(f"Cannot add {type(other)} to EnumerationDateVar") 

469 result_var = EnumerationDateVar( 

470 f"{self.name}_plus_{other.years}y_{other.months}m_{other.days}d" 

471 ) 

472 result_var._lazy_op = ("add", self, other) 

473 if self._value is not None: 

474 py_date = self._value.to_python_date() 

475 result_date = py_date + relativedelta( 

476 years=other.years, months=other.months, days=other.days 

477 ) 

478 result = Date.from_python_date(result_date) 

479 result_var.set_value(result.year, result.month, result.day) 

480 return result_var 

481 

482 def __sub__(self, other: Period) -> "EnumerationDateVar": 

483 if not isinstance(other, Period): 483 ↛ 484line 483 didn't jump to line 484 because the condition on line 483 was never true

484 raise TypeError(f"Cannot subtract {type(other)} from EnumerationDateVar") 

485 result_var = EnumerationDateVar( 

486 f"{self.name}_minus_{other.years}y_{other.months}m_{other.days}d" 

487 ) 

488 result_var._lazy_op = ("sub", self, other) 

489 if self._value is not None: 489 ↛ 490line 489 didn't jump to line 490 because the condition on line 489 was never true

490 py_date = self._value.to_python_date() 

491 neg_period = Period(-other.years, -other.months, -other.days) 

492 result_date = py_date + relativedelta( 

493 years=neg_period.years, months=neg_period.months, days=neg_period.days 

494 ) 

495 result = Date.from_python_date(result_date) 

496 result_var.set_value(result.year, result.month, result.day) 

497 return result_var 

498 

499 # Expose year/month/day components for constraint building (date-only support) 

500 @property 

501 def year(self) -> "EnumerationDateComponent": 

502 return EnumerationDateComponent(self, "year") 

503 

504 @property 

505 def month(self) -> "EnumerationDateComponent": 

506 return EnumerationDateComponent(self, "month") 

507 

508 @property 

509 def day(self) -> "EnumerationDateComponent": 

510 return EnumerationDateComponent(self, "day") 

511 

512 

513class EnumerationDateComponent: 

514 """Int-like view for a date component (year/month/day) used only in date constraints.""" 

515 

516 def __init__(self, parent: EnumerationDateVar, attr: str): 

517 self.parent = parent 

518 self.attr = attr 

519 

520 def _get_component_value(self) -> Optional[int]: 

521 """Get the current value of this component.""" 

522 d = self.parent.get_value() 

523 if d is None: 523 ↛ 524line 523 didn't jump to line 524 because the condition on line 523 was never true

524 return None 

525 return getattr(d, self.attr) 

526 

527 def _compare( 

528 self, 

529 op: str, 

530 other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"], 

531 ): 

532 def compare(): 

533 lhs_val = self._get_component_value() 

534 if lhs_val is None: 534 ↛ 535line 534 didn't jump to line 535 because the condition on line 534 was never true

535 return False 

536 

537 # Handle EnumerationDateComponent 

538 if isinstance(other, EnumerationDateComponent): 538 ↛ 539line 538 didn't jump to line 539 because the condition on line 538 was never true

539 rhs_val = other._get_component_value() 

540 if rhs_val is None: 

541 return False 

542 return getattr(lhs_val, f"__{op}__")(rhs_val) 

543 # Handle EnumerationComponentExpr 

544 elif isinstance(other, EnumerationComponentExpr): 544 ↛ 545line 544 didn't jump to line 545 because the condition on line 544 was never true

545 rhs_val = other.evaluate() 

546 if rhs_val is None: 

547 return False 

548 return getattr(lhs_val, f"__{op}__")(rhs_val) 

549 # Handle int 

550 else: 

551 return getattr(lhs_val, f"__{op}__")(int(other)) 

552 

553 return compare 

554 

555 def __eq__(self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"]) -> ConstraintWrapper: # type: ignore[override] 

556 return ConstraintWrapper(self._compare("eq", other)) 

557 

558 def __ne__(self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"]) -> ConstraintWrapper: # type: ignore[override] 

559 return ConstraintWrapper(self._compare("ne", other)) 

560 

561 def __lt__( 

562 self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"] 

563 ) -> ConstraintWrapper: 

564 return ConstraintWrapper(self._compare("lt", other)) 

565 

566 def __le__( 

567 self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"] 

568 ) -> ConstraintWrapper: 

569 return ConstraintWrapper(self._compare("le", other)) 

570 

571 def __gt__( 

572 self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"] 

573 ) -> ConstraintWrapper: 

574 return ConstraintWrapper(self._compare("gt", other)) 

575 

576 def __ge__( 

577 self, other: Union[int, "EnumerationDateComponent", "EnumerationComponentExpr"] 

578 ) -> ConstraintWrapper: 

579 return ConstraintWrapper(self._compare("ge", other)) 

580 

581 # Arithmetic operations for expressions like x.year + 4 

582 def __add__(self, other: int) -> "EnumerationComponentExpr": 

583 if not isinstance(other, int): 

584 raise TypeError(f"Cannot add {type(other)} to EnumerationDateComponent") 

585 # Create a wrapper that evaluates the component value and adds the constant 

586 base_var = EnumerationComponentVar( 

587 f"{self.parent.name}_{self.attr}", 

588 1900 if self.attr == "year" else 1, 

589 2100 if self.attr == "year" else (12 if self.attr == "month" else 31), 

590 self.attr, 

591 ) 

592 # Store reference to the parent component for lazy evaluation 

593 base_var._parent_component = self 

594 return EnumerationComponentExpr(base_var, "add", other) 

595 

596 def __sub__( 

597 self, other: Union[int, "EnumerationDateComponent"] 

598 ) -> "EnumerationComponentExpr": 

599 # Handle subtraction of another EnumerationDateComponent (e.g., y.year - x.year) 

600 if isinstance(other, EnumerationDateComponent): 

601 base_var = EnumerationComponentVar( 

602 f"{self.parent.name}_{self.attr}", 

603 1900 if self.attr == "year" else 1, 

604 2100 if self.attr == "year" else (12 if self.attr == "month" else 31), 

605 self.attr, 

606 ) 

607 base_var._parent_component = self 

608 expr = EnumerationComponentExpr(base_var, "sub_component", 0) 

609 expr._other_component = other 

610 return expr 

611 # Handle subtraction of an integer 

612 elif isinstance(other, int): 

613 base_var = EnumerationComponentVar( 

614 f"{self.parent.name}_{self.attr}", 

615 1900 if self.attr == "year" else 1, 

616 2100 if self.attr == "year" else (12 if self.attr == "month" else 31), 

617 self.attr, 

618 ) 

619 base_var._parent_component = self 

620 return EnumerationComponentExpr(base_var, "sub", other) 

621 else: 

622 raise TypeError( 

623 f"Cannot subtract {type(other)} from EnumerationDateComponent" 

624 ) 

625 

626 def __mul__(self, other: int) -> "EnumerationComponentExpr": 

627 if not isinstance(other, int): 

628 raise TypeError( 

629 f"Cannot multiply {type(other)} by EnumerationDateComponent" 

630 ) 

631 base_var = EnumerationComponentVar( 

632 f"{self.parent.name}_{self.attr}", 

633 1900 if self.attr == "year" else 1, 

634 2100 if self.attr == "year" else (12 if self.attr == "month" else 31), 

635 self.attr, 

636 ) 

637 base_var._parent_component = self 

638 return EnumerationComponentExpr(base_var, "mul", other) 

639 

640 def __mod__(self, other: int) -> "EnumerationComponentExpr": 

641 if not isinstance(other, int): 

642 raise TypeError( 

643 f"Cannot compute modulo with {type(other)} on EnumerationDateComponent" 

644 ) 

645 base_var = EnumerationComponentVar( 

646 f"{self.parent.name}_{self.attr}", 

647 1900 if self.attr == "year" else 1, 

648 2100 if self.attr == "year" else (12 if self.attr == "month" else 31), 

649 self.attr, 

650 ) 

651 base_var._parent_component = self 

652 return EnumerationComponentExpr(base_var, "mod", other) 

653 

654 def __truediv__(self, other: int) -> "EnumerationComponentExpr": 

655 """Integer division using / operator (implements floor division semantics).""" 

656 if not isinstance(other, int): 

657 raise TypeError(f"Cannot divide {type(other)} by EnumerationDateComponent") 

658 base_var = EnumerationComponentVar( 

659 f"{self.parent.name}_{self.attr}", 

660 1900 if self.attr == "year" else 1, 

661 2100 if self.attr == "year" else (12 if self.attr == "month" else 31), 

662 self.attr, 

663 ) 

664 base_var._parent_component = self 

665 return EnumerationComponentExpr(base_var, "floordiv", other) 

666 

667 

668class EnumerationSolver: 

669 """Solver that enumerates all valid dates and checks constraints.""" 

670 

671 MIN_DATE = date(1900, 3, 1) 

672 MAX_DATE = date(2100, 2, 28) 

673 

674 def __init__(self, timeout_ms: int = 60000): 

675 self.date_vars: Dict[str, EnumerationDateVar] = {} 

676 self.component_vars: Dict[str, EnumerationComponentVar] = ( 

677 {} 

678 ) # Component variables - e.x. Date(y, 1, 1) 

679 self.constraints: List[Any] = [] 

680 self.timeout_ms = timeout_ms 

681 self._cached_valid_dates: Optional[List[date]] = None 

682 

683 def add_date_var(self, name: str) -> EnumerationDateVar: 

684 """Declare a new symbolic date variable. 

685 

686 The variable will enumerate all valid dates in the allowed range. 

687 

688 Args: 

689 name: Variable name (must be unique) 

690 

691 Returns: 

692 The created EnumerationDateVar 

693 

694 Raises: 

695 ValueError: If a variable with this name already exists 

696 """ 

697 if name in self.date_vars: 697 ↛ 698line 697 didn't jump to line 698 because the condition on line 697 was never true

698 raise ValueError(f"Date variable '{name}' already declared") 

699 

700 date_var = EnumerationDateVar(name) 

701 self.date_vars[name] = date_var 

702 return date_var 

703 

704 def add_year_var(self, name: str) -> EnumerationComponentVar: 

705 """Declare a symbolic year variable that enumerates from 1900 to 2100. 

706 

707 Raises: 

708 ValueError: If a component variable with this name already exists 

709 """ 

710 if name in self.component_vars: 710 ↛ 711line 710 didn't jump to line 711 because the condition on line 710 was never true

711 raise ValueError(f"Component variable '{name}' already declared") 

712 

713 var = EnumerationComponentVar(name, 1900, 2100, "year") 

714 self.component_vars[name] = var 

715 return var 

716 

717 def add_month_var(self, name: str) -> EnumerationComponentVar: 

718 """Declare a symbolic month variable that enumerates from 1 to 12. 

719 

720 Raises: 

721 ValueError: If a component variable with this name already exists 

722 """ 

723 if name in self.component_vars: 723 ↛ 724line 723 didn't jump to line 724 because the condition on line 723 was never true

724 raise ValueError(f"Component variable '{name}' already declared") 

725 

726 var = EnumerationComponentVar(name, 1, 12, "month") 

727 self.component_vars[name] = var 

728 return var 

729 

730 def add_day_var(self, name: str) -> EnumerationComponentVar: 

731 """Declare a symbolic day variable that enumerates from 1 to 31. 

732 

733 Raises: 

734 ValueError: If a component variable with this name already exists 

735 """ 

736 if name in self.component_vars: 736 ↛ 737line 736 didn't jump to line 737 because the condition on line 736 was never true

737 raise ValueError(f"Component variable '{name}' already declared") 

738 

739 var = EnumerationComponentVar(name, 1, 31, "day") 

740 self.component_vars[name] = var 

741 return var 

742 

743 def add_int_var( 

744 self, 

745 name: str, 

746 min_value: int = None, 

747 max_value: int = None, 

748 component_type: str = None, 

749 ) -> EnumerationComponentVar: 

750 """Declare a symbolic integer variable for use in Date() constructors. 

751 

752 Integer variables should only be used in Date() constructors (e.g., Date(l, 1, 1)). 

753 The bounds are automatically set based on the component type: 

754 - 'year': 1900-2100 

755 - 'month': 1-12 

756 - 'day': 1-31 

757 

758 Args: 

759 name: Variable name (must be unique) 

760 min_value: Optional minimum value (overrides component_type defaults if provided) 

761 max_value: Optional maximum value (overrides component_type defaults if provided) 

762 component_type: Optional component type ('year', 'month', 'day'). If provided, 

763 sets appropriate bounds. If not provided, uses min_value/max_value 

764 or defaults to 0-8000. 

765 

766 Returns: 

767 The created EnumerationComponentVar 

768 

769 Raises: 

770 ValueError: If a component variable with this name already exists 

771 """ 

772 if name in self.component_vars: 

773 raise ValueError(f"Integer variable '{name}' already declared") 

774 

775 # Set bounds based on component type if provided 

776 if component_type == "year": 

777 if min_value is None: 

778 min_value = 1900 

779 if max_value is None: 

780 max_value = 2100 

781 elif component_type == "month": 

782 if min_value is None: 

783 min_value = 1 

784 if max_value is None: 

785 max_value = 12 

786 elif component_type == "day": 

787 if min_value is None: 

788 min_value = 1 

789 if max_value is None: 

790 max_value = 31 

791 else: 

792 # Default bounds (should not be used for Date() components) 

793 if min_value is None: 

794 min_value = 0 

795 if max_value is None: 

796 max_value = 8000 

797 

798 var = EnumerationComponentVar( 

799 name, min_value, max_value, component_type or "int" 

800 ) 

801 self.component_vars[name] = var 

802 return var 

803 

804 def SymbolicDate(self, year, month, day) -> EnumerationDateVar: 

805 """Create a date with symbolic components. 

806 

807 Args: 

808 year: int, EnumerationComponentVar, or EnumerationComponentExpr 

809 month: int, EnumerationComponentVar, or EnumerationComponentExpr 

810 day: int, EnumerationComponentVar, or EnumerationComponentExpr 

811 

812 Returns: 

813 EnumerationDateVar that will be enumerated based on symbolic components 

814 

815 Example: 

816 year_var = solver.add_year_var("y") 

817 d = solver.SymbolicDate(year_var, 1, 1) # Only enumerates years 

818 solver.add_constraint(x < d) 

819 """ 

820 # Generate a unique name for this symbolic date 

821 symbolic_parts = [] 

822 if isinstance(year, (EnumerationComponentVar, EnumerationComponentExpr)): 

823 symbolic_parts.append("y") 

824 if isinstance(month, (EnumerationComponentVar, EnumerationComponentExpr)): 

825 symbolic_parts.append("m") 

826 if isinstance(day, (EnumerationComponentVar, EnumerationComponentExpr)): 

827 symbolic_parts.append("d") 

828 

829 name = f"symdate_{''.join(symbolic_parts)}_{len(self.date_vars)}" 

830 date_var = EnumerationDateVar(name) 

831 

832 # Store component specifications (can be int, EnumerationComponentVar, or EnumerationComponentExpr) 

833 date_var._year_spec = year 

834 date_var._month_spec = month 

835 date_var._day_spec = day 

836 

837 self.date_vars[name] = date_var 

838 return date_var 

839 

840 def add_constraint(self, constraint: Any) -> None: 

841 self.constraints.append(constraint) 

842 

843 def Or(self, *args) -> ConstraintWrapper: 

844 """Wrapper for Z3's Or() that creates a ConstraintWrapper with or_constraints.""" 

845 return Or_enumeration(*args) 

846 

847 def And(self, *args) -> ConstraintWrapper: 

848 """Wrapper for Z3's And() that creates a ConstraintWrapper.""" 

849 return And_enumeration(*args) 

850 

851 def Not(self, arg) -> ConstraintWrapper: 

852 """Wrapper for Z3's Not() that creates a ConstraintWrapper.""" 

853 return Not_enumeration(arg) 

854 

855 def Implies(self, antecedent, consequent) -> ConstraintWrapper: 

856 """Wrapper for Z3's Implies() that creates a ConstraintWrapper.""" 

857 return Implies_enumeration(antecedent, consequent) 

858 

859 def get_execution_context(self) -> Dict[str, Any]: 

860 """Get execution context dictionary for running generated constraint code. 

861 

862 This sets up Or, And, Not, Implies to use the enumeration baseline's 

863 wrapper functions instead of Z3's functions, so boolean operators work 

864 correctly with ConstraintWrapper objects. 

865 

866 Returns: 

867 Dictionary suitable for use as exec_globals when executing generated code 

868 """ 

869 

870 # Create a mock z3 module with our wrapper functions 

871 class MockZ3: 

872 Or = self.Or 

873 And = self.And 

874 Not = self.Not 

875 Implies = self.Implies 

876 Int = lambda *args: None # Not used by enumeration baseline solving 

877 Bool = lambda *args: None # Not used by enumeration baseline solving 

878 

879 # Override __import__ to return our mock z3 module 

880 original_import = builtins.__import__ 

881 

882 def mock_import(name, *args, **kwargs): 

883 if name == "z3": 

884 return MockZ3() 

885 return original_import(name, *args, **kwargs) 

886 

887 return { 

888 "Date": Date, 

889 "Period": Period, 

890 "SymbolicDate": self.SymbolicDate, 

891 "DateSATBuilder": lambda: self, 

892 "builder": self, 

893 "__builtins__": {**builtins.__dict__, "__import__": mock_import}, 

894 "And": self.And, 

895 "Or": self.Or, 

896 "Not": self.Not, 

897 "Implies": self.Implies, 

898 } 

899 

900 def check(self) -> str: 

901 try: 

902 solution = self._find_solution() 

903 return "sat" if solution is not None else "unsat" 

904 except TimeoutError: 

905 return "timeout" # ★ cleaner status 

906 

907 def solve(self) -> Dict[str, Any]: 

908 try: 

909 solution = self._find_solution() 

910 if solution is not None: 910 ↛ 913line 910 didn't jump to line 913 because the condition on line 910 was always true

911 return {"status": "sat", "dates": solution} 

912 else: 

913 return {"status": "unsat", "dates": {}} 

914 except TimeoutError as e: 

915 return {"status": "timeout", "reason": str(e), "dates": {}} 

916 

917 def model(self) -> Dict[str, Any]: 

918 out = self.solve() 

919 return out 

920 

921 def get_concrete_dates(self) -> Dict[str, Date]: 

922 out = self.solve() 

923 return out.get("dates", {}) 

924 

925 def to_smt2(self) -> str: 

926 return "; Enumeration baseline - no SMT-LIB output" 

927 

928 def get_assertions(self) -> list: 

929 return self.constraints 

930 

931 # ------------------------- internals ------------------------- 

932 

933 def _find_solution(self) -> Optional[Dict[str, Date]]: 

934 """Brute-force enumeration: enumerate all possible dates for all variables. 

935 

936 Pure enumeration baseline - no optimizations. Enumerates all combinations 

937 of dates for all base variables and checks which satisfy the constraints. 

938 

939 Supports both full date variables and dates with symbolic components. 

940 For dates with symbolic components (e.g., Date(year_var, 1, 1)), only 

941 the symbolic components are enumerated, making it much more efficient. 

942 

943 If no variables are declared, evaluate constraints concretely using datetime. 

944 Otherwise, enumerate all possible date combinations for all declared variables. 

945 """ 

946 var_names = list(self.date_vars.keys()) 

947 

948 # If no variables, evaluate constraints concretely using datetime 

949 if not var_names: 949 ↛ 950line 949 didn't jump to line 950 because the condition on line 949 was never true

950 return {} if self._evaluate_constraints({}) else None 

951 

952 # Register intermediate lazy nodes (created by operations like x + Period) 

953 # These are derived variables, not base variables 

954 for c in self.constraints: 

955 if isinstance(c, ConstraintWrapper): 955 ↛ 954line 955 didn't jump to line 954 because the condition on line 955 was always true

956 targets = c.or_constraints if c.or_constraints is not None else [c] 

957 for sub_c in targets: 

958 # Only register lazy nodes (have _lazy_op), not undeclared base variables 

959 if sub_c.var_ref is not None and isinstance( 

960 sub_c.var_ref, EnumerationDateVar 

961 ): 

962 if ( 962 ↛ 966line 962 didn't jump to line 966 because the condition on line 962 was never true

963 hasattr(sub_c.var_ref, "_lazy_op") 

964 and sub_c.var_ref.name not in self.date_vars 

965 ): 

966 self.date_vars[sub_c.var_ref.name] = sub_c.var_ref 

967 if sub_c.rhs_ref is not None and isinstance( 

968 sub_c.rhs_ref, EnumerationDateVar 

969 ): 

970 if ( 

971 hasattr(sub_c.rhs_ref, "_lazy_op") 

972 and sub_c.rhs_ref.name not in self.date_vars 

973 ): 

974 self.date_vars[sub_c.rhs_ref.name] = sub_c.rhs_ref 

975 

976 # Get all base variables (skip intermediate lazy nodes) 

977 # NO BINDING OPTIMIZATION - enumerate all base variables 

978 base_vars: List[str] = [] 

979 for name in list(self.date_vars.keys()): 

980 var = self.date_vars[name] 

981 # skip intermediate lazy nodes (they have _lazy_op attribute) 

982 if hasattr(var, "_lazy_op"): 

983 continue 

984 base_vars.append(name) 

985 

986 if not base_vars: 986 ↛ 988line 986 didn't jump to line 988 because the condition on line 986 was never true

987 # No base variables to enumerate - evaluate constraints with empty assignment 

988 return {} if self._evaluate_constraints({}) else None 

989 

990 # Build candidate lists for each variable 

991 # Variables with symbolic components get specialized enumeration 

992 candidate_lists = [] 

993 for vname in base_vars: 

994 var = self.date_vars[vname] 

995 candidates = self._generate_candidates_for_var(var) 

996 candidate_lists.append(candidates) 

997 

998 # Calculate search space 

999 space = 1 

1000 for candidates in candidate_lists: 

1001 space *= len(candidates) 

1002 if space > 1_000_000: 

1003 print(f"Warning: Large search space ({space} combinations).") 

1004 

1005 start = time.time() 

1006 timeout_s = self.timeout_ms / 1000.0 

1007 

1008 # Enumerate all combinations - pure brute force 

1009 for combo in itertools.product(*candidate_lists): 1009 ↛ 1096line 1009 didn't jump to line 1096 because the loop on line 1009 didn't complete

1010 if (time.time() - start) > timeout_s: 

1011 raise TimeoutError( 

1012 f"Enumeration timeout after {time.time() - start:.2f}s (limit: {timeout_s:.2f}s)" 

1013 ) 

1014 

1015 # Create assignment: map each variable to a date 

1016 assignment: Dict[str, Date] = {} 

1017 valid_assignment = True 

1018 

1019 for vname, candidate in zip(base_vars, combo): 

1020 var = self.date_vars[vname] 

1021 

1022 # Handle symbolic component dates 

1023 if hasattr(var, "_year_spec"): 

1024 # Check if any component is symbolic 

1025 has_symbolic = ( 

1026 isinstance( 

1027 var._year_spec, 

1028 (EnumerationComponentVar, EnumerationComponentExpr), 

1029 ) 

1030 or isinstance( 

1031 var._month_spec, 

1032 (EnumerationComponentVar, EnumerationComponentExpr), 

1033 ) 

1034 or isinstance( 

1035 var._day_spec, 

1036 (EnumerationComponentVar, EnumerationComponentExpr), 

1037 ) 

1038 ) 

1039 

1040 if has_symbolic: 1040 ↛ 1078line 1040 didn't jump to line 1078 because the condition on line 1040 was always true

1041 # candidate is a tuple (year, month, day, base_year, base_month, base_day) 

1042 # The base values are for setting the component variables 

1043 year, month, day, base_year, base_month, base_day = candidate 

1044 # Set component variable values (use base values, not expression results) 

1045 if isinstance( 

1046 var._year_spec, 

1047 (EnumerationComponentVar, EnumerationComponentExpr), 

1048 ): 

1049 base_var = self._get_base_component_var(var._year_spec) 

1050 if base_var: 1050 ↛ 1052line 1050 didn't jump to line 1052 because the condition on line 1050 was always true

1051 base_var.set_value(base_year) 

1052 if isinstance( 

1053 var._month_spec, 

1054 (EnumerationComponentVar, EnumerationComponentExpr), 

1055 ): 

1056 base_var = self._get_base_component_var(var._month_spec) 

1057 if base_var: 1057 ↛ 1059line 1057 didn't jump to line 1059 because the condition on line 1057 was always true

1058 base_var.set_value(base_month) 

1059 if isinstance( 

1060 var._day_spec, 

1061 (EnumerationComponentVar, EnumerationComponentExpr), 

1062 ): 

1063 base_var = self._get_base_component_var(var._day_spec) 

1064 if base_var: 1064 ↛ 1067line 1064 didn't jump to line 1067 because the condition on line 1064 was always true

1065 base_var.set_value(base_day) 

1066 

1067 try: 

1068 date_obj = Date(year, month, day) 

1069 assignment[vname] = date_obj 

1070 var.set_value(year, month, day) 

1071 except ValueError: 

1072 # Invalid date (e.g., Feb 31), skip this combination 

1073 valid_assignment = False 

1074 break 

1075 else: 

1076 # SymbolicDate but no symbolic components - shouldn't happen 

1077 # Regular date variable - candidate is already a python date 

1078 assignment[vname] = Date( 

1079 candidate.year, candidate.month, candidate.day 

1080 ) 

1081 var.set_value(candidate.year, candidate.month, candidate.day) 

1082 else: 

1083 # Regular date variable - candidate is already a python date 

1084 assignment[vname] = Date( 

1085 candidate.year, candidate.month, candidate.day 

1086 ) 

1087 var.set_value(candidate.year, candidate.month, candidate.day) 

1088 

1089 if not valid_assignment: 

1090 continue 

1091 

1092 # Evaluate constraints with this assignment 

1093 if self._evaluate_constraints(assignment): 

1094 return assignment 

1095 

1096 return None 

1097 

1098 def _generate_all_valid_dates(self) -> List[date]: 

1099 if self._cached_valid_dates is None: 

1100 dates: List[date] = [] 

1101 current = self.MIN_DATE 

1102 while current <= self.MAX_DATE: 

1103 try: 

1104 Date(current.year, current.month, current.day) 

1105 dates.append(current) 

1106 except ValueError: 

1107 pass 

1108 current += timedelta(days=1) 

1109 self._cached_valid_dates = dates 

1110 return self._cached_valid_dates 

1111 

1112 def _get_base_component_var(self, component) -> Optional[EnumerationComponentVar]: 

1113 """Get the base component variable from a component (handles expressions too).""" 

1114 if isinstance(component, EnumerationComponentVar): 

1115 return component 

1116 elif isinstance(component, EnumerationComponentExpr): 1116 ↛ 1118line 1116 didn't jump to line 1118 because the condition on line 1116 was always true

1117 return component.get_base_var() 

1118 return None 

1119 

1120 def _evaluate_component(self, component) -> Optional[int]: 

1121 """Evaluate a component to get its integer value.""" 

1122 if isinstance(component, int): 

1123 return component 

1124 elif isinstance(component, EnumerationComponentVar): 

1125 return component.get_value() 

1126 elif isinstance(component, EnumerationComponentExpr): 

1127 return component.evaluate() 

1128 return None 

1129 

1130 def _generate_candidates_for_var( 

1131 self, var: EnumerationDateVar 

1132 ) -> List[Union[date, Tuple[int, int, int, int, int, int]]]: 

1133 """Generate candidate values for a variable. 

1134 

1135 For regular date variables, returns list of python dates. 

1136 For symbolic component dates, returns list of (year, month, day, base_year, base_month, base_day) tuples. 

1137 The base values are what the underlying component variables should be set to. 

1138 """ 

1139 # Check if this is a symbolic component date 

1140 if not hasattr(var, "_year_spec"): 

1141 # Regular date variable - enumerate all valid dates 

1142 return self._generate_all_valid_dates() 

1143 

1144 # Check if any component is symbolic 

1145 has_symbolic = ( 

1146 isinstance( 

1147 var._year_spec, (EnumerationComponentVar, EnumerationComponentExpr) 

1148 ) 

1149 or isinstance( 

1150 var._month_spec, (EnumerationComponentVar, EnumerationComponentExpr) 

1151 ) 

1152 or isinstance( 

1153 var._day_spec, (EnumerationComponentVar, EnumerationComponentExpr) 

1154 ) 

1155 ) 

1156 

1157 if not has_symbolic: 1157 ↛ 1159line 1157 didn't jump to line 1159 because the condition on line 1157 was never true

1158 # SymbolicDate but no symbolic components - shouldn't happen 

1159 return self._generate_all_valid_dates() 

1160 

1161 # Symbolic component date - enumerate only symbolic components 

1162 year_ranges = self._get_component_ranges(var._year_spec, "year") 

1163 month_ranges = self._get_component_ranges(var._month_spec, "month") 

1164 day_ranges = self._get_component_ranges(var._day_spec, "day") 

1165 

1166 # Generate all combinations of (year, month, day, base_year, base_month, base_day) 

1167 # Note: We don't validate here, validation happens during enumeration 

1168 # This allows us to skip invalid dates like Feb 31 

1169 candidates = [] 

1170 for y_tuple in year_ranges: 

1171 for m_tuple in month_ranges: 

1172 for d_tuple in day_ranges: 

1173 y_val, y_base = y_tuple 

1174 m_val, m_base = m_tuple 

1175 d_val, d_base = d_tuple 

1176 candidates.append((y_val, m_val, d_val, y_base, m_base, d_base)) 

1177 

1178 return candidates 

1179 

1180 def _get_component_ranges( 

1181 self, component_spec, component_type: str 

1182 ) -> List[Tuple[int, int]]: 

1183 """Get the range of values for a date component. 

1184 

1185 Args: 

1186 component_spec: int, EnumerationComponentVar, or EnumerationComponentExpr 

1187 component_type: 'year', 'month', or 'day' (for fallback ranges) 

1188 

1189 Returns list of (evaluated_value, base_value) tuples. 

1190 - evaluated_value: the actual value to use in the Date() constructor 

1191 - base_value: the value to set in the underlying EnumerationComponentVar 

1192 """ 

1193 if isinstance( 

1194 component_spec, (EnumerationComponentVar, EnumerationComponentExpr) 

1195 ): 

1196 # Symbolic component - get base variable and enumerate its range 

1197 base_var = self._get_base_component_var(component_spec) 

1198 if base_var: 1198 ↛ 1241line 1198 didn't jump to line 1241 because the condition on line 1198 was always true

1199 base_range = range(base_var.min_val, base_var.max_val + 1) 

1200 

1201 if isinstance(component_spec, EnumerationComponentExpr): 

1202 # For expressions like year_var + 1, enumerate base variable 

1203 # and compute the expression result 

1204 results = [] 

1205 for base_val in base_range: 

1206 if component_spec.op == "add": 1206 ↛ 1208line 1206 didn't jump to line 1208 because the condition on line 1206 was always true

1207 eval_val = base_val + component_spec.constant 

1208 elif component_spec.op == "sub": 

1209 eval_val = base_val - component_spec.constant 

1210 elif component_spec.op == "mul": 

1211 eval_val = base_val * component_spec.constant 

1212 elif component_spec.op == "mod": 

1213 if component_spec.constant == 0: 

1214 continue # Skip division by zero 

1215 eval_val = base_val % component_spec.constant 

1216 elif component_spec.op == "floordiv": 

1217 if component_spec.constant == 0: 

1218 continue # Skip division by zero 

1219 eval_val = base_val // component_spec.constant 

1220 elif component_spec.op == "sub_component": 

1221 # This is a subtraction of another component (e.g., y.year - x.year) 

1222 # We can't enumerate this directly since it depends on two different parents 

1223 # Return a placeholder - the actual value will be computed during constraint evaluation 

1224 # For enumeration, we'll use a wide range that covers all possible differences 

1225 # The actual difference will be computed when both parent components have values 

1226 # Use a conservative range based on component type 

1227 if component_type == "year": 

1228 # Year differences can range from -200 to 200 (2100 - 1900) 

1229 eval_val = base_val # Placeholder, will be evaluated at constraint time 

1230 else: 

1231 eval_val = base_val 

1232 else: 

1233 eval_val = base_val 

1234 results.append((eval_val, base_val)) 

1235 return results 

1236 else: 

1237 # Simple variable, base value and evaluated value are the same 

1238 return [(v, v) for v in base_range] 

1239 

1240 # Concrete component (int) - single value, no base variable 

1241 if isinstance(component_spec, int): 1241 ↛ 1245line 1241 didn't jump to line 1245 because the condition on line 1241 was always true

1242 return [(component_spec, component_spec)] 

1243 

1244 # Fallback - shouldn't happen but be safe 

1245 if component_type == "year": 

1246 vals = list(range(1900, 2101)) 

1247 elif component_type == "month": 

1248 vals = list(range(1, 13)) 

1249 else: # day 

1250 vals = list(range(1, 32)) 

1251 return [(v, v) for v in vals] 

1252 

1253 def _evaluate_constraints(self, assignment: Dict[str, Date]) -> bool: 

1254 try: 

1255 # Register intermediate lazy nodes for evaluation 

1256 for c in self.constraints: 

1257 if isinstance(c, ConstraintWrapper): 1257 ↛ 1256line 1257 didn't jump to line 1256 because the condition on line 1257 was always true

1258 # Handle OR constraints - check all sub-constraints 

1259 targets = c.or_constraints if c.or_constraints is not None else [c] 

1260 for sub_c in targets: 

1261 # Only register lazy nodes, not undeclared base variables 

1262 if sub_c.var_ref is not None and isinstance( 

1263 sub_c.var_ref, EnumerationDateVar 

1264 ): 

1265 if ( 1265 ↛ 1269line 1265 didn't jump to line 1269 because the condition on line 1265 was never true

1266 hasattr(sub_c.var_ref, "_lazy_op") 

1267 and sub_c.var_ref.name not in self.date_vars 

1268 ): 

1269 self.date_vars[sub_c.var_ref.name] = sub_c.var_ref 

1270 if sub_c.rhs_ref is not None and isinstance( 

1271 sub_c.rhs_ref, EnumerationDateVar 

1272 ): 

1273 if ( 1273 ↛ 1277line 1273 didn't jump to line 1277 because the condition on line 1273 was never true

1274 hasattr(sub_c.rhs_ref, "_lazy_op") 

1275 and sub_c.rhs_ref.name not in self.date_vars 

1276 ): 

1277 self.date_vars[sub_c.rhs_ref.name] = sub_c.rhs_ref 

1278 

1279 # Reset lazy nodes so they recompute from base values 

1280 for _, var in list(self.date_vars.items()): 

1281 if hasattr(var, "_lazy_op"): 

1282 var._hard_reset_value() # ★ 

1283 

1284 # Set all base variable values for this assignment 

1285 for name, val in assignment.items(): 

1286 if name in self.date_vars and isinstance(val, Date): 1286 ↛ 1285line 1286 didn't jump to line 1285 because the condition on line 1286 was always true

1287 self.date_vars[name].set_value(val.year, val.month, val.day) 

1288 

1289 # Evaluate 

1290 for c in self.constraints: 

1291 if isinstance(c, bool): 1291 ↛ 1292line 1291 didn't jump to line 1292 because the condition on line 1291 was never true

1292 if not c: 

1293 return False 

1294 elif isinstance(c, ConstraintWrapper): 1294 ↛ 1297line 1294 didn't jump to line 1297 because the condition on line 1294 was always true

1295 if not c.evaluate(): 

1296 return False 

1297 elif callable(c): 

1298 if not c(): 

1299 return False 

1300 else: 

1301 # last resort: bool() — but many types (like ConstraintWrapper) forbid __bool__ 

1302 if not bool(c): 

1303 return False 

1304 return True 

1305 except (ValueError, TypeError): 

1306 return False