crosshair-tool 0.0.56__cp39-cp39-macosx_11_0_arm64.whl → 0.0.100__cp39-cp39-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. _crosshair_tracers.cpython-39-darwin.so +0 -0
  2. crosshair/__init__.py +1 -1
  3. crosshair/_mark_stacks.h +51 -24
  4. crosshair/_tracers.h +9 -5
  5. crosshair/_tracers_test.py +19 -9
  6. crosshair/auditwall.py +9 -8
  7. crosshair/auditwall_test.py +31 -19
  8. crosshair/codeconfig.py +3 -2
  9. crosshair/condition_parser.py +17 -133
  10. crosshair/condition_parser_test.py +54 -96
  11. crosshair/conftest.py +1 -1
  12. crosshair/copyext.py +91 -22
  13. crosshair/copyext_test.py +33 -0
  14. crosshair/core.py +259 -203
  15. crosshair/core_and_libs.py +20 -0
  16. crosshair/core_regestered_types_test.py +82 -0
  17. crosshair/core_test.py +693 -664
  18. crosshair/diff_behavior.py +76 -21
  19. crosshair/diff_behavior_test.py +132 -23
  20. crosshair/dynamic_typing.py +128 -18
  21. crosshair/dynamic_typing_test.py +91 -4
  22. crosshair/enforce.py +1 -6
  23. crosshair/enforce_test.py +15 -23
  24. crosshair/examples/check_examples_test.py +2 -1
  25. crosshair/fnutil.py +2 -3
  26. crosshair/fnutil_test.py +0 -7
  27. crosshair/fuzz_core_test.py +70 -83
  28. crosshair/libimpl/arraylib.py +10 -7
  29. crosshair/libimpl/binascii_ch_test.py +30 -0
  30. crosshair/libimpl/binascii_test.py +67 -0
  31. crosshair/libimpl/binasciilib.py +150 -0
  32. crosshair/libimpl/bisectlib_test.py +5 -5
  33. crosshair/libimpl/builtinslib.py +1002 -682
  34. crosshair/libimpl/builtinslib_ch_test.py +108 -30
  35. crosshair/libimpl/builtinslib_test.py +431 -143
  36. crosshair/libimpl/codecslib.py +22 -2
  37. crosshair/libimpl/codecslib_test.py +41 -9
  38. crosshair/libimpl/collectionslib.py +44 -8
  39. crosshair/libimpl/collectionslib_test.py +108 -20
  40. crosshair/libimpl/copylib.py +1 -1
  41. crosshair/libimpl/copylib_test.py +18 -0
  42. crosshair/libimpl/datetimelib.py +84 -67
  43. crosshair/libimpl/datetimelib_ch_test.py +12 -7
  44. crosshair/libimpl/datetimelib_test.py +5 -6
  45. crosshair/libimpl/decimallib.py +5257 -0
  46. crosshair/libimpl/decimallib_ch_test.py +78 -0
  47. crosshair/libimpl/decimallib_test.py +76 -0
  48. crosshair/libimpl/encodings/_encutil.py +21 -11
  49. crosshair/libimpl/fractionlib.py +16 -0
  50. crosshair/libimpl/fractionlib_test.py +80 -0
  51. crosshair/libimpl/functoolslib.py +19 -7
  52. crosshair/libimpl/functoolslib_test.py +22 -6
  53. crosshair/libimpl/hashliblib.py +30 -0
  54. crosshair/libimpl/hashliblib_test.py +18 -0
  55. crosshair/libimpl/heapqlib.py +32 -5
  56. crosshair/libimpl/heapqlib_test.py +15 -12
  57. crosshair/libimpl/iolib.py +7 -4
  58. crosshair/libimpl/ipaddresslib.py +8 -0
  59. crosshair/libimpl/itertoolslib_test.py +1 -1
  60. crosshair/libimpl/mathlib.py +165 -2
  61. crosshair/libimpl/mathlib_ch_test.py +44 -0
  62. crosshair/libimpl/mathlib_test.py +59 -16
  63. crosshair/libimpl/oslib.py +7 -0
  64. crosshair/libimpl/pathliblib_test.py +10 -0
  65. crosshair/libimpl/randomlib.py +1 -0
  66. crosshair/libimpl/randomlib_test.py +6 -4
  67. crosshair/libimpl/relib.py +180 -59
  68. crosshair/libimpl/relib_ch_test.py +26 -2
  69. crosshair/libimpl/relib_test.py +77 -14
  70. crosshair/libimpl/timelib.py +35 -13
  71. crosshair/libimpl/timelib_test.py +13 -3
  72. crosshair/libimpl/typeslib.py +15 -0
  73. crosshair/libimpl/typeslib_test.py +36 -0
  74. crosshair/libimpl/unicodedatalib_test.py +3 -3
  75. crosshair/libimpl/weakreflib.py +13 -0
  76. crosshair/libimpl/weakreflib_test.py +69 -0
  77. crosshair/libimpl/zliblib.py +15 -0
  78. crosshair/libimpl/zliblib_test.py +13 -0
  79. crosshair/lsp_server.py +21 -10
  80. crosshair/main.py +48 -28
  81. crosshair/main_test.py +59 -14
  82. crosshair/objectproxy.py +39 -14
  83. crosshair/objectproxy_test.py +27 -13
  84. crosshair/opcode_intercept.py +212 -24
  85. crosshair/opcode_intercept_test.py +172 -18
  86. crosshair/options.py +0 -1
  87. crosshair/patch_equivalence_test.py +5 -21
  88. crosshair/path_cover.py +7 -5
  89. crosshair/path_search.py +6 -4
  90. crosshair/path_search_test.py +1 -2
  91. crosshair/pathing_oracle.py +53 -10
  92. crosshair/pathing_oracle_test.py +21 -0
  93. crosshair/pure_importer_test.py +5 -21
  94. crosshair/register_contract.py +16 -6
  95. crosshair/register_contract_test.py +2 -14
  96. crosshair/simplestructs.py +154 -85
  97. crosshair/simplestructs_test.py +16 -2
  98. crosshair/smtlib.py +24 -0
  99. crosshair/smtlib_test.py +14 -0
  100. crosshair/statespace.py +319 -196
  101. crosshair/statespace_test.py +45 -0
  102. crosshair/stubs_parser.py +0 -2
  103. crosshair/test_util.py +87 -25
  104. crosshair/test_util_test.py +26 -0
  105. crosshair/tools/check_init_and_setup_coincide.py +0 -3
  106. crosshair/tools/generate_demo_table.py +2 -2
  107. crosshair/tracers.py +141 -49
  108. crosshair/type_repo.py +11 -4
  109. crosshair/unicode_categories.py +1 -0
  110. crosshair/util.py +158 -76
  111. crosshair/util_test.py +13 -20
  112. crosshair/watcher.py +4 -4
  113. crosshair/z3util.py +1 -1
  114. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/METADATA +45 -36
  115. crosshair_tool-0.0.100.dist-info/RECORD +176 -0
  116. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/WHEEL +2 -1
  117. crosshair/examples/hypothesis/__init__.py +0 -2
  118. crosshair/examples/hypothesis/bugs_detected/simple_strategies.py +0 -74
  119. crosshair_tool-0.0.56.dist-info/RECORD +0 -152
  120. /crosshair/{examples/hypothesis/bugs_detected/__init__.py → py.typed} +0 -0
  121. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/entry_points.txt +0 -0
  122. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info/licenses}/LICENSE +0 -0
  123. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/top_level.txt +0 -0
crosshair/statespace.py CHANGED
@@ -10,6 +10,7 @@ from collections import Counter, defaultdict
10
10
  from dataclasses import dataclass
11
11
  from sys import _getframe
12
12
  from time import monotonic
13
+ from traceback import extract_stack, format_tb
13
14
  from types import FrameType
14
15
  from typing import (
15
16
  Any,
@@ -17,6 +18,7 @@ from typing import (
17
18
  Dict,
18
19
  List,
19
20
  NewType,
21
+ NoReturn,
20
22
  Optional,
21
23
  Sequence,
22
24
  Set,
@@ -29,18 +31,22 @@ import z3 # type: ignore
29
31
 
30
32
  from crosshair import dynamic_typing
31
33
  from crosshair.condition_parser import ConditionExpr
34
+ from crosshair.smtlib import parse_smtlib_literal
32
35
  from crosshair.tracers import NoTracing, ResumedTracing, is_tracing
33
36
  from crosshair.util import (
34
- CrosshairInternal,
37
+ CROSSHAIR_EXTRA_ASSERTS,
38
+ CrossHairInternal,
35
39
  IgnoreAttempt,
40
+ NotDeterministic,
36
41
  PathTimeout,
37
42
  UnknownSatisfiability,
43
+ assert_tracing,
44
+ ch_stack,
38
45
  debug,
39
46
  in_debug,
40
47
  name_of_type,
41
- test_stack,
42
48
  )
43
- from crosshair.z3util import z3Aassert, z3Not, z3PopNot
49
+ from crosshair.z3util import z3Aassert, z3Not, z3Or, z3PopNot
44
50
 
45
51
 
46
52
  @functools.total_ordering
@@ -159,14 +165,19 @@ def model_value_to_python(value: z3.ExprRef) -> object:
159
165
  if value.num_args() == 1:
160
166
  ret.append(model_value_to_python(value.arg(0)))
161
167
  return ret
162
- elif z3.is_int(value):
168
+ elif z3.is_fp(value):
169
+ return parse_smtlib_literal(value.sexpr())
170
+ elif hasattr(value, "py_value"):
171
+ # TODO: how many other cases could be handled with py_value nowadays?
172
+ return value.py_value()
173
+ elif z3.is_int(value): # catch for older z3 versions that don't have py_value
163
174
  return value.as_long()
164
175
  else:
165
176
  return ast.literal_eval(repr(value))
166
177
 
167
178
 
179
+ @assert_tracing(False)
168
180
  def prefer_true(v: Any) -> bool:
169
- assert not is_tracing()
170
181
  if not (hasattr(v, "var") and z3.is_bool(v.var)):
171
182
  with ResumedTracing():
172
183
  v = v.__bool__()
@@ -176,6 +187,22 @@ def prefer_true(v: Any) -> bool:
176
187
  return space.choose_possible(v.var, probability_true=1.0)
177
188
 
178
189
 
190
+ def force_true(v: Any) -> None:
191
+ with NoTracing():
192
+ if not (hasattr(v, "var") and z3.is_bool(v.var)):
193
+ with ResumedTracing():
194
+ v = v.__bool__()
195
+ if not (hasattr(v, "var")):
196
+ raise CrossHairInternal(
197
+ "Attempted to call assert_true on a non-symbolic"
198
+ )
199
+ space = context_statespace()
200
+ # TODO: we can improve this by making a new kind of (unary) assertion node
201
+ # that would not create these useless forks when the space is near exhaustion.
202
+ if not space.choose_possible(v.var, probability_true=1.0):
203
+ raise IgnoreAttempt
204
+
205
+
179
206
  class StateSpaceCounter(Counter):
180
207
  @property
181
208
  def iterations(self) -> int:
@@ -194,12 +221,8 @@ class StateSpaceCounter(Counter):
194
221
  return "{" + ", ".join(parts) + "}"
195
222
 
196
223
 
197
- class NotDeterministic(Exception):
198
- pass
199
-
200
-
201
224
  class AbstractPathingOracle:
202
- def pre_path_hook(self, root: "RootNode") -> None:
225
+ def pre_path_hook(self, space: "StateSpace") -> None:
203
226
  pass
204
227
 
205
228
  def post_path_hook(self, path: Sequence["SearchTreeNode"]) -> None:
@@ -225,14 +248,16 @@ class StateSpaceContext:
225
248
 
226
249
  def __enter__(self):
227
250
  prev = real_getattr(_THREAD_LOCALS, "space", None)
228
- assert prev is None, "Already in a state space context"
251
+ if prev is not None:
252
+ raise CrossHairInternal("Already in a state space context")
229
253
  space = self.space
230
254
  _THREAD_LOCALS.space = space
231
255
  space.mark_all_parent_frames()
232
256
 
233
257
  def __exit__(self, exc_type, exc_value, tb):
234
258
  prev = real_getattr(_THREAD_LOCALS, "space", None)
235
- assert prev is self.space, "State space was altered in context"
259
+ if prev is not self.space:
260
+ raise CrossHairInternal("State space was altered in context")
236
261
  _THREAD_LOCALS.space = None
237
262
  return False
238
263
 
@@ -244,7 +269,7 @@ def optional_context_statespace() -> Optional["StateSpace"]:
244
269
  def context_statespace() -> "StateSpace":
245
270
  space = _THREAD_LOCALS.space
246
271
  if space is None:
247
- raise CrosshairInternal
272
+ raise CrossHairInternal("Not in a statespace context")
248
273
  return space
249
274
 
250
275
 
@@ -268,59 +293,27 @@ class NodeLike:
268
293
  """
269
294
  raise NotImplementedError
270
295
 
271
- def is_stem(self) -> bool:
272
- return False
273
-
274
- def grow_into(self, node: _N) -> _N:
275
- raise NotImplementedError
276
-
277
- def simplify(self) -> "NodeLike":
278
- return self
279
-
280
296
  def stats(self) -> StateSpaceCounter:
281
297
  raise NotImplementedError
282
298
 
299
+ children_fields: Tuple[str, ...] = ()
283
300
 
284
- class NodeStem(NodeLike):
285
- evolution: Optional["SearchTreeNode"] = None
286
-
287
- def is_exhausted(self) -> bool:
288
- return False if self.evolution is None else self.evolution.is_exhausted()
289
-
290
- def get_result(self) -> CallAnalysis:
291
- return (
292
- CallAnalysis(VerificationStatus.UNKNOWN)
293
- if self.evolution is None
294
- else self.evolution.get_result()
295
- )
296
-
297
- def is_stem(self) -> bool:
298
- return self.evolution is None
299
-
300
- def grow_into(self, node: _N) -> _N:
301
- self.evolution = node
302
- return node
303
-
304
- def simplify(self):
305
- return self if self.evolution is None else self.evolution
306
-
307
- def stats(self) -> StateSpaceCounter:
308
- return StateSpaceCounter() if self.evolution is None else self.evolution.stats()
309
-
310
- def __repr__(self) -> str:
311
- return "NodeStem()"
301
+ def replace_child(self, current_child, replacement_child: "NodeLike") -> None:
302
+ for child_field in self.children_fields:
303
+ child = getattr(self, child_field)
304
+ if child is current_child:
305
+ setattr(self, child_field, replacement_child)
306
+ return
307
+ raise CrossHairInternal(f"Child {current_child} not found in {self}")
312
308
 
313
309
 
314
310
  class SearchTreeNode(NodeLike):
315
- """
316
- Represent a single decision point.
317
-
318
- Abstract helper class for StateSpace.
319
- """
311
+ """A node in the execution path tree."""
320
312
 
321
313
  stacktail: Tuple[str, ...] = ()
322
314
  result: CallAnalysis = CallAnalysis()
323
315
  exhausted: bool = False
316
+ iteration: Optional[int] = None
324
317
 
325
318
  def choose(
326
319
  self, space: "StateSpace", probability_true: Optional[float] = None
@@ -345,13 +338,34 @@ class SearchTreeNode(NodeLike):
345
338
  raise NotImplementedError
346
339
 
347
340
 
348
- def solver_is_sat(solver, *a) -> bool:
349
- ret = solver.check(*a)
341
+ class NodeStem(NodeLike):
342
+ def __init__(self, parent: SearchTreeNode, parent_attr_name: str):
343
+ self.parent = parent
344
+ self.parent_attr_name = parent_attr_name
345
+
346
+ def is_exhausted(self) -> bool:
347
+ return False
348
+
349
+ def get_result(self) -> CallAnalysis:
350
+ return CallAnalysis(VerificationStatus.UNKNOWN)
351
+
352
+ def stats(self) -> StateSpaceCounter:
353
+ return StateSpaceCounter()
354
+
355
+ def __repr__(self) -> str:
356
+ return "NodeStem()"
357
+
358
+
359
+ def solver_is_sat(solver, *exprs) -> bool:
360
+ ret = solver.check(*exprs)
350
361
  if ret == z3.unknown:
351
- debug("Z3 unknown sat reason:", solver.reason_unknown())
362
+ debug("Z3 Unknown satisfiability. Reason:", solver.reason_unknown())
363
+ debug("Call stack at time of unknown sat:", ch_stack())
352
364
  if solver.reason_unknown() == "interrupted from keyboard":
353
365
  raise KeyboardInterrupt
354
- debug("Unknown satisfiability. Solver state follows:\n", solver.sexpr())
366
+ if exprs:
367
+ debug("While attempting to assert\n", *(e.sexpr() for e in exprs))
368
+ debug("Solver state follows:\n", solver.sexpr())
355
369
  raise UnknownSatisfiability
356
370
  return ret == z3.sat
357
371
 
@@ -390,7 +404,7 @@ class SinglePathNode(SearchTreeNode):
390
404
 
391
405
  def __init__(self, decision: bool):
392
406
  self.decision = decision
393
- self.child = NodeStem()
407
+ self.child = NodeStem(self, "child")
394
408
  self._random = newrandom()
395
409
 
396
410
  def choose(
@@ -399,12 +413,14 @@ class SinglePathNode(SearchTreeNode):
399
413
  return (self.decision, 1.0, self.child)
400
414
 
401
415
  def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
402
- self.child = self.child.simplify()
416
+ assert isinstance(self.child, SearchTreeNode)
403
417
  return (self.child.get_result(), self.child.is_exhausted())
404
418
 
405
419
  def stats(self) -> StateSpaceCounter:
406
420
  return self.child.stats()
407
421
 
422
+ children_fields = ("child",)
423
+
408
424
 
409
425
  class BranchCounter:
410
426
  __slots__ = ["pos_ct", "neg_ct"]
@@ -424,10 +440,11 @@ class RootNode(SinglePathNode):
424
440
  )
425
441
  from crosshair.pathing_oracle import CoveragePathingOracle # circular import
426
442
 
427
- self.pathing_oracle = CoveragePathingOracle()
443
+ self.pathing_oracle: AbstractPathingOracle = CoveragePathingOracle()
444
+ self.iteration = 0
428
445
 
429
446
 
430
- class DeatchedPathNode(SinglePathNode):
447
+ class DetachedPathNode(SinglePathNode):
431
448
  def __init__(self):
432
449
  super().__init__(True)
433
450
  # Seems like `exhausted` should be True, but we set to False until we can
@@ -437,7 +454,6 @@ class DeatchedPathNode(SinglePathNode):
437
454
  self._stats = None
438
455
 
439
456
  def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
440
- self.child = self.child.simplify()
441
457
  return (leaf_analysis, True)
442
458
 
443
459
  def stats(self) -> StateSpaceCounter:
@@ -467,13 +483,15 @@ class BinaryPathNode(SearchTreeNode):
467
483
  def stats(self) -> StateSpaceCounter:
468
484
  return self._stats
469
485
 
486
+ children_fields = ("negative", "positive")
487
+
470
488
 
471
489
  class RandomizedBinaryPathNode(BinaryPathNode):
472
490
  def __init__(self, rand: random.Random):
473
491
  super().__init__()
474
492
  self._random = rand
475
- self.positive = NodeStem()
476
- self.negative = NodeStem()
493
+ self.positive = NodeStem(self, "positive")
494
+ self.negative = NodeStem(self, "negative")
477
495
 
478
496
  def probability_true(
479
497
  self, space: "StateSpace", requested_probability: Optional[float] = None
@@ -498,10 +516,6 @@ class RandomizedBinaryPathNode(BinaryPathNode):
498
516
  else:
499
517
  return (positive_ok, 1.0, self.positive if positive_ok else self.negative)
500
518
 
501
- def _simplify(self) -> None:
502
- self.positive = self.positive.simplify()
503
- self.negative = self.negative.simplify()
504
-
505
519
 
506
520
  class ParallelNode(RandomizedBinaryPathNode):
507
521
  """Choose either path; the first complete result will be used."""
@@ -515,7 +529,6 @@ class ParallelNode(RandomizedBinaryPathNode):
515
529
  return f"ParallelNode(false_pct={self._false_probability}, {self._desc})"
516
530
 
517
531
  def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
518
- self._simplify()
519
532
  positive, negative = self.positive, self.negative
520
533
  pos_exhausted = positive.is_exhausted()
521
534
  neg_exhausted = negative.is_exhausted()
@@ -592,13 +605,11 @@ class WorstResultNode(RandomizedBinaryPathNode):
592
605
  if not solver_is_sat(solver, expr):
593
606
  self.forced_path = False
594
607
  else:
595
- # TODO: we still run into soundness issues on occasion, so I'd like to
596
- # leave _PERFORM_EXTRA_SAT_CHECKS enabled a little longer:
597
- _PERFORM_EXTRA_SAT_CHECKS = True
598
- if _PERFORM_EXTRA_SAT_CHECKS and not solver_is_sat(solver, expr):
608
+ # We run into soundness issues on occasion:
609
+ if CROSSHAIR_EXTRA_ASSERTS and not solver_is_sat(solver, expr):
599
610
  debug(" *** Reached impossible code path *** ")
600
611
  debug("Current solver state:\n", str(solver))
601
- raise CrosshairInternal("Reached impossible code path")
612
+ raise CrossHairInternal("Reached impossible code path")
602
613
  self.forced_path = True
603
614
  self.expr = expr
604
615
 
@@ -635,7 +646,6 @@ class WorstResultNode(RandomizedBinaryPathNode):
635
646
  )
636
647
 
637
648
  def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
638
- self._simplify()
639
649
  positive, negative = self.positive, self.negative
640
650
  exhausted = self._is_exhausted()
641
651
  if node_status(positive) == VerificationStatus.REFUTED or (
@@ -660,7 +670,7 @@ class ModelValueNode(WorstResultNode):
660
670
  def __init__(self, rand: random.Random, expr: z3.ExprRef, solver: z3.Solver):
661
671
  if not solver_is_sat(solver):
662
672
  debug("Solver unexpectedly unsat; solver state:", solver.sexpr())
663
- raise CrosshairInternal("Unexpected unsat from solver")
673
+ raise CrossHairInternal("Unexpected unsat from solver")
664
674
 
665
675
  self.condition_value = solver.model().evaluate(expr, model_completion=True)
666
676
  self._stats_key = f"realize_{expr}" if z3.is_const(expr) else None
@@ -677,7 +687,6 @@ class ModelValueNode(WorstResultNode):
677
687
 
678
688
  def debug_path_tree(node, highlights, prefix="") -> List[str]:
679
689
  highlighted = node in highlights
680
- node = node.simplify()
681
690
  highlighted |= node in highlights
682
691
  if isinstance(node, BinaryPathNode):
683
692
  if isinstance(node, WorstResultNode) and node.forced_path is not None:
@@ -710,6 +719,17 @@ def debug_path_tree(node, highlights, prefix="") -> List[str]:
710
719
  return [f"{prefix} -> {str(node)} {node.stats()}"]
711
720
 
712
721
 
722
+ def make_default_solver() -> z3.Solver:
723
+ """Create a new solver with default settings."""
724
+ smt_tactic = z3.Tactic("smt")
725
+ solver = smt_tactic.solver()
726
+ solver.set("mbqi", True)
727
+ # turn off every randomization thing we can think of:
728
+ solver.set("random-seed", 42)
729
+ solver.set("smt.random-seed", 42)
730
+ return solver
731
+
732
+
713
733
  class StateSpace:
714
734
  """Holds various information about the SMT solver's current state."""
715
735
 
@@ -723,16 +743,12 @@ class StateSpace:
723
743
  model_check_timeout: float,
724
744
  search_root: RootNode,
725
745
  ):
726
- smt_timeout = model_check_timeout * 1000 + 1
727
- smt_tactic = z3.Tactic("smt")
728
- if smt_timeout < 1 << 63:
729
- smt_tactic = z3.TryFor(smt_tactic, int(smt_timeout))
730
- self.solver = smt_tactic.solver()
731
- self.solver.set(mbqi=True)
732
- # turn off every randomization thing we can think of:
733
- self.solver.set("random-seed", 42)
734
- self.solver.set("smt.random-seed", 42)
735
- # self.solver.set('randomize', False)
746
+ self.solver = make_default_solver()
747
+ if model_check_timeout < 1 << 63:
748
+ self.smt_timeout: Optional[int] = int(model_check_timeout * 1000 + 1)
749
+ self.solver.set(timeout=self.smt_timeout)
750
+ else:
751
+ self.smt_timeout = None
736
752
  self.choices_made: List[SearchTreeNode] = []
737
753
  self.status_cap: Optional[VerificationStatus] = None
738
754
  self.heaps: List[List[Tuple[z3.ExprRef, Type, object]]] = [[]]
@@ -747,17 +763,30 @@ class StateSpace:
747
763
  self._random = search_root._random
748
764
  _, _, self._search_position = search_root.choose(self)
749
765
  self._deferred_assumptions = []
750
- search_root.pathing_oracle.pre_path_hook(search_root)
766
+ assert search_root.iteration is not None
767
+ search_root.iteration += 1
768
+ search_root.pathing_oracle.pre_path_hook(self)
751
769
 
752
- def add(self, expr: z3.ExprRef) -> None:
770
+ def add(self, expr) -> None:
753
771
  with NoTracing():
772
+ if hasattr(expr, "var"):
773
+ expr = expr.var
774
+ elif not isinstance(expr, z3.ExprRef):
775
+ if type(expr) is bool:
776
+ raise CrossHairInternal(
777
+ "Attempted to assert a concrete boolean (look for unexpected realization)"
778
+ )
779
+ raise CrossHairInternal(
780
+ "Expected symbolic boolean, but supplied expression of type",
781
+ name_of_type(type(expr)),
782
+ )
754
783
  # debug('Committed to ', expr)
755
784
  already_known = self._exprs_known.get(expr)
756
785
  if already_known is None:
757
786
  self.solver.add(expr)
758
787
  self._exprs_known[expr] = True
759
788
  elif already_known is not True:
760
- raise CrosshairInternal
789
+ raise CrossHairInternal
761
790
 
762
791
  def rand(self) -> random.Random:
763
792
  return self._random
@@ -771,31 +800,42 @@ class StateSpace:
771
800
  return value # type: ignore
772
801
 
773
802
  def stats_lookahead(self) -> Tuple[StateSpaceCounter, StateSpaceCounter]:
774
- node = self._search_position.simplify()
775
- if node.is_stem():
803
+ node = self._search_position
804
+ if isinstance(node, NodeStem):
776
805
  return (StateSpaceCounter(), StateSpaceCounter())
777
- assert isinstance(
778
- node, BinaryPathNode
779
- ), f"node {node} {node.is_stem()} is not a binarypathnode"
806
+ assert isinstance(node, BinaryPathNode), f"node {node} is not a binarypathnode"
780
807
  return node.stats_lookahead()
781
808
 
809
+ def grow_into(self, node: _N) -> _N:
810
+ assert isinstance(self._search_position, NodeStem)
811
+ self._search_position.parent.replace_child(self._search_position, node)
812
+ node.iteration = self._root.iteration
813
+ self._search_position = node
814
+ return node
815
+
782
816
  def fork_parallel(self, false_probability: float, desc: str = "") -> bool:
783
- if self._search_position.is_stem():
784
- node: NodeLike = self._search_position.grow_into(
785
- ParallelNode(self._random, false_probability, desc)
786
- )
817
+ node = self._search_position
818
+ if isinstance(node, NodeStem):
819
+ node = self.grow_into(ParallelNode(self._random, false_probability, desc))
820
+ node.stacktail = self.gen_stack_descriptions()
787
821
  assert isinstance(node, ParallelNode)
788
822
  self._search_position = node
789
823
  else:
790
- node = self._search_position.simplify()
791
- assert isinstance(node, ParallelNode)
824
+ if not isinstance(node, ParallelNode):
825
+ self.raise_not_deterministic(
826
+ node, "Wrong node type (expected ParallelNode)"
827
+ )
792
828
  node._false_probability = false_probability
793
829
  self.choices_made.append(node)
794
830
  ret, _, next_node = node.choose(self)
795
831
  self._search_position = next_node
796
832
  return ret
797
833
 
798
- def is_possible(self, expr: z3.ExprRef) -> bool:
834
+ def is_possible(self, expr) -> bool:
835
+ with NoTracing():
836
+ if hasattr(expr, "var"):
837
+ expr = expr.var
838
+ debug("is possible?", expr)
799
839
  return solver_is_sat(self.solver, expr)
800
840
 
801
841
  def mark_all_parent_frames(self):
@@ -811,15 +851,7 @@ class StateSpace:
811
851
 
812
852
  def gen_stack_descriptions(self) -> Tuple[str, ...]:
813
853
  f: Any = _getframe().f_back.f_back # type: ignore
814
- # TODO: if we deprecate 3.7, we could try this instead of the above:
815
- # frames = [f := f.f_back or f for _ in range(8)]
816
- frames = []
817
- external_frame_ids = self.external_frame_ids
818
- for _ in range(8):
819
- if id(f) in external_frame_ids:
820
- break
821
- frames.append(f)
822
- f = f.f_back
854
+ frames = [f := f.f_back or f for _ in range(8)]
823
855
  # TODO: To help oracles, I'd like to add sub-line resolution via f.f_lasti;
824
856
  # however, in Python >= 3.11, the instruction pointer can shift between
825
857
  # PRECALL and CALL opcodes, triggering our nondeterminism check.
@@ -834,66 +866,45 @@ class StateSpace:
834
866
  )
835
867
  raise PathTimeout
836
868
 
869
+ @assert_tracing(False)
837
870
  def choose_possible(
838
871
  self, expr: z3.ExprRef, probability_true: Optional[float] = None
839
872
  ) -> bool:
840
873
  known_result = self._exprs_known.get(expr)
841
874
  if isinstance(known_result, bool):
842
875
  return known_result
843
- if is_tracing():
844
- raise CrosshairInternal
845
- if self._search_position.is_stem():
846
- # We only allow time outs at stems - that's because we don't want
847
- # to think about how mutating an existing path branch would work:
848
- self.check_timeout()
849
- node = self._search_position.grow_into(
850
- WorstResultNode(self._random, expr, self.solver)
851
- )
852
- else:
853
- node = self._search_position.simplify() # type: ignore
854
- if not isinstance(node, WorstResultNode):
855
- debug(" *** Begin Not Deterministic Debug *** ")
856
- debug(" Traceback: ", test_stack())
857
- debug("Decision expression:")
858
- debug(f" {expr}")
859
- debug("Now found at incompatible node of type:")
860
- debug(f" {type(node)}")
861
- debug(" *** End Not Deterministic Debug *** ")
862
- raise NotDeterministic
863
- if not z3.eq(node.expr, expr):
864
- debug(" *** Begin Not Deterministic Debug *** ")
865
- debug(" Traceback: ", test_stack())
866
- debug("Decision expression changed from:")
867
- debug(f" {node.expr}")
868
- debug("To:")
869
- debug(f" {expr}")
870
- debug(" *** End Not Deterministic Debug *** ")
871
- raise NotDeterministic
872
-
873
- self._search_position = node
874
876
  # NOTE: format_stack() is more human readable, but it pulls source file contents,
875
877
  # so it is (1) slow, and (2) unstable when source code changes while we are checking.
876
878
  stacktail = self.gen_stack_descriptions()
877
- assert isinstance(node, SearchTreeNode)
878
- if not node.stacktail:
879
- node.stacktail = stacktail
879
+ if isinstance(self._search_position, SearchTreeNode):
880
+ node = self._search_position
881
+ not_deterministic_reason = (
882
+ (
883
+ (not isinstance(node, WorstResultNode))
884
+ and f"Wrong node type (is {name_of_type(type(node))}, expected WorstResultNode)"
885
+ )
886
+ # TODO: Not clear whether we want this stack trace check.
887
+ # A stack change usually indicates a serious problem, but not 100% of the time.
888
+ # Keeping it would mean that we fail earlier.
889
+ # But also see https://github.com/HypothesisWorks/hypothesis/pull/4034#issuecomment-2606415404
890
+ # or (node.stacktail != stacktail and "Stack trace changed")
891
+ or (
892
+ (hasattr(node, "expr") and (not z3.eq(node.expr, expr)))
893
+ and "SMT expression changed"
894
+ )
895
+ )
896
+ if not_deterministic_reason:
897
+ self.raise_not_deterministic(
898
+ node, not_deterministic_reason, expr=expr, stacktail=stacktail
899
+ )
880
900
  else:
881
- if node.stacktail != stacktail:
882
- debug(" *** Begin Not Deterministic Debug *** ")
883
- debug(" First state: ")
884
- debug(node.stacktail)
885
- debug(" Current state: ")
886
- debug(stacktail)
887
- debug(" Decision points prior to this:")
888
- for choice in self.choices_made:
889
- debug(" ", choice)
890
- debug(" Stack Diff: ")
891
- import difflib
892
-
893
- debug("\n".join(difflib.context_diff(node.stacktail, stacktail)))
894
- debug(" *** End Not Deterministic Debug *** ")
895
- raise NotDeterministic
901
+ # We only allow time outs at stems - that's because we don't want
902
+ # to think about how mutating an existing path branch would work:
903
+ self.check_timeout()
904
+ node = self.grow_into(WorstResultNode(self._random, expr, self.solver))
905
+ node.stacktail = stacktail
896
906
 
907
+ self._search_position = node
897
908
  choose_true, chosen_probability, stem = node.choose(
898
909
  self, probability_true=probability_true
899
910
  )
@@ -909,32 +920,64 @@ class StateSpace:
909
920
  chosen_expr = expr if choose_true else z3Not(expr)
910
921
  if in_debug():
911
922
  debug(
912
- "SMT chose:",
913
- chosen_expr,
914
- "(chance:",
915
- chosen_probability,
916
- ")",
923
+ f"SMT chose: {chosen_expr} (chance: {chosen_probability}) at",
924
+ ch_stack(),
917
925
  )
918
926
  z3Aassert(self.solver, chosen_expr)
919
927
  self._exprs_known[expr] = choose_true
920
928
  return choose_true
921
929
 
930
+ def raise_not_deterministic(
931
+ self,
932
+ node: NodeLike,
933
+ reason: str,
934
+ expr: Optional[z3.ExprRef] = None,
935
+ stacktail: Optional[Tuple[str, ...]] = None,
936
+ currently_handling: Optional[BaseException] = None,
937
+ ) -> NoReturn:
938
+ lines = ["*** Begin Not Deterministic Debug ***"]
939
+ if getattr(node, "iteration", None) is not None:
940
+ lines.append(f"Previous iteration: {node.iteration}") # type: ignore
941
+ if hasattr(node, "expr"):
942
+ lines.append(f"Previous SMT expression: {node.expr}") # type: ignore
943
+ if expr is not None:
944
+ lines.append(f"Current SMT expression: {expr}")
945
+ if not stacktail:
946
+ if currently_handling is not None:
947
+ stacktail = tuple(format_tb(currently_handling.__traceback__))
948
+ else:
949
+ stacktail = self.gen_stack_descriptions()
950
+ lines.append("Current stack tail:")
951
+ lines.extend(f" {x}" for x in stacktail)
952
+ if hasattr(node, "stacktail"):
953
+ lines.append("Previous stack tail:")
954
+ lines.extend(f" {x}" for x in node.stacktail)
955
+ lines.append(f"Reason: {reason}")
956
+ lines.append("*** End Not Deterministic Debug ***")
957
+ for line in lines:
958
+ print(line)
959
+ exc = NotDeterministic()
960
+ if currently_handling:
961
+ raise exc from currently_handling
962
+ else:
963
+ raise exc
964
+
922
965
  def find_model_value(self, expr: z3.ExprRef) -> Any:
923
966
  with NoTracing():
924
967
  while True:
925
- if self._search_position.is_stem():
926
- self._search_position = self._search_position.grow_into(
968
+ if isinstance(self._search_position, NodeStem):
969
+ self._search_position = self.grow_into(
927
970
  ModelValueNode(self._random, expr, self.solver)
928
971
  )
929
- node = self._search_position.simplify()
972
+ node = self._search_position
930
973
  if isinstance(node, SearchLeaf):
931
- raise CrosshairInternal(
974
+ raise CrossHairInternal(
932
975
  f"Cannot use symbolics; path is already terminated"
933
976
  )
934
977
  if not isinstance(node, ModelValueNode):
935
978
  debug(" *** Begin Not Deterministic Debug *** ")
936
979
  debug(f"Model value node expected; found {type(node)} instead.")
937
- debug(" Traceback: ", test_stack())
980
+ debug(" Traceback: ", ch_stack())
938
981
  debug(" *** End Not Deterministic Debug *** ")
939
982
  raise NotDeterministic
940
983
  (chosen, _, next_node) = node.choose(self, probability_true=1.0)
@@ -950,14 +993,14 @@ class StateSpace:
950
993
  ):
951
994
  self._already_logged.add(expr)
952
995
  debug("SMT realized symbolic:", expr, "==", repr(ret))
953
- debug("Realized at", test_stack())
996
+ debug("Realized at", ch_stack())
954
997
  return ret
955
998
  else:
956
999
  self.solver.add(expr != node.condition_value)
957
1000
 
958
1001
  def find_model_value_for_function(self, expr: z3.ExprRef) -> object:
959
1002
  if not solver_is_sat(self.solver):
960
- raise CrosshairInternal("model unexpectedly became unsatisfiable")
1003
+ raise CrossHairInternal("model unexpectedly became unsatisfiable")
961
1004
  # TODO: this need to go into a tree node that returns UNKNOWN or worse
962
1005
  # (because it just returns one example function; it's not covering the space)
963
1006
 
@@ -989,7 +1032,7 @@ class StateSpace:
989
1032
  ) -> object:
990
1033
  with NoTracing():
991
1034
  # TODO: needs more testing
992
- for (curref, curtyp, curval) in self.heaps[snapshot]:
1035
+ for curref, curtyp, curval in self.heaps[snapshot]:
993
1036
 
994
1037
  # TODO: using unify() is almost certainly wrong; just because the types
995
1038
  # have some instances in common does not mean that `curval` actually
@@ -999,7 +1042,7 @@ class StateSpace:
999
1042
  continue
1000
1043
  if self.smt_fork(curref == ref, probability_true=0.1):
1001
1044
  debug(
1002
- "HEAP key lookup ",
1045
+ "Heap key lookup ",
1003
1046
  ref,
1004
1047
  ": Found existing. ",
1005
1048
  "type:",
@@ -1010,7 +1053,7 @@ class StateSpace:
1010
1053
  return curval
1011
1054
  ret = proxy_generator(typ)
1012
1055
  debug(
1013
- "HEAP key lookup ",
1056
+ "Heap key lookup ",
1014
1057
  ref,
1015
1058
  ": Created new. ",
1016
1059
  "type:",
@@ -1026,6 +1069,51 @@ class StateSpace:
1026
1069
  self.next_uniq += 1
1027
1070
  return "_{:x}".format(self.next_uniq)
1028
1071
 
1072
+ @assert_tracing(False)
1073
+ def smt_fanout(
1074
+ self,
1075
+ exprs_and_results: Sequence[Tuple[z3.ExprRef, object]],
1076
+ desc: str,
1077
+ weights: Optional[Sequence[float]] = None,
1078
+ none_of_the_above_weight: float = 0.0,
1079
+ ):
1080
+ """Performs a weighted binary search over the given SMT expressions."""
1081
+ exprs = [e for (e, _) in exprs_and_results]
1082
+ final_weights = [1.0] * len(exprs) if weights is None else weights
1083
+ if CROSSHAIR_EXTRA_ASSERTS:
1084
+ if len(final_weights) != len(exprs):
1085
+ raise CrossHairInternal("inconsistent smt_fanout exprs and weights")
1086
+ if not all(0 < w for w in final_weights):
1087
+ raise CrossHairInternal("smt_fanout weights must be greater than zero")
1088
+ if not self.is_possible(z3Or(*exprs)):
1089
+ raise CrossHairInternal(
1090
+ "no smt_fanout option is possible: " + repr(exprs)
1091
+ )
1092
+ if self.is_possible(z3Not(z3Or(*exprs))):
1093
+ raise CrossHairInternal(
1094
+ "smt_fanout options are not exhaustive: " + repr(exprs)
1095
+ )
1096
+
1097
+ def attempt(start: int, end: int):
1098
+ size = end - start
1099
+ if size == 1:
1100
+ return exprs_and_results[start][1]
1101
+ mid = (start + end) // 2
1102
+ left_exprs = exprs[start:mid]
1103
+ left_weight = sum(final_weights[start:mid])
1104
+ right_weight = sum(final_weights[mid:end])
1105
+ if self.smt_fork(
1106
+ z3Or(*left_exprs),
1107
+ probability_true=left_weight / (left_weight + right_weight),
1108
+ desc=f"{desc}_fan_size_{size}",
1109
+ ):
1110
+ return attempt(start, mid)
1111
+ else:
1112
+ return attempt(mid, end)
1113
+
1114
+ return attempt(0, len(exprs))
1115
+
1116
+ @assert_tracing(False)
1029
1117
  def smt_fork(
1030
1118
  self,
1031
1119
  expr: Optional[z3.ExprRef] = None,
@@ -1039,7 +1127,15 @@ class StateSpace:
1039
1127
  def defer_assumption(self, description: str, checker: Callable[[], bool]) -> None:
1040
1128
  self._deferred_assumptions.append((description, checker))
1041
1129
 
1042
- def detach_path(self) -> None:
1130
+ def extend_timeouts(
1131
+ self, constant_factor: float = 0.0, smt_multiple: Optional[float] = None
1132
+ ) -> None:
1133
+ self.execution_deadline += constant_factor
1134
+ if self.smt_timeout is not None and smt_multiple is not None:
1135
+ self.smt_timeout = int(self.smt_timeout * smt_multiple)
1136
+ self.solver.set(timeout=self.smt_timeout)
1137
+
1138
+ def detach_path(self, currently_handling: Optional[BaseException] = None) -> None:
1043
1139
  """
1044
1140
  Mark the current path exhausted.
1045
1141
 
@@ -1052,36 +1148,62 @@ class StateSpace:
1052
1148
  if self.is_detached:
1053
1149
  debug("Path is already detached")
1054
1150
  return
1055
- else:
1056
- # Give ourselves a time extension for deferred assumptions and
1057
- # (likely) counterexample generation to follow.
1058
- self.execution_deadline += 2.0
1151
+ # Give ourselves a time extension for deferred assumptions and
1152
+ # (likely) counterexample generation to follow.
1153
+ self.extend_timeouts(constant_factor=4.0, smt_multiple=2.0)
1154
+ self.is_detached = True
1155
+ if isinstance(
1156
+ currently_handling,
1157
+ (NotDeterministic, UnknownSatisfiability, PathTimeout),
1158
+ ):
1159
+ # These exceptions can happen at any time; we may not be at a stem node.
1160
+ node = DetachedPathNode()
1161
+ self.choices_made.append(node)
1162
+ self._search_position = node.child
1163
+ return
1059
1164
  for description, checker in self._deferred_assumptions:
1060
1165
  with ResumedTracing():
1061
1166
  check_ret = checker()
1062
1167
  if not prefer_true(check_ret):
1168
+ node = self.grow_into(DetachedPathNode())
1169
+ self.choices_made.append(node)
1170
+ self._search_position = node.child
1063
1171
  raise IgnoreAttempt("deferred assumption failed: " + description)
1064
- self.is_detached = True
1065
- assert self._search_position.is_stem()
1066
- node = self._search_position.grow_into(DeatchedPathNode())
1067
- assert node.child.is_stem()
1172
+ if not isinstance(self._search_position, NodeStem):
1173
+ # Nondeterminism detected
1174
+ # We'll just overwrite the prior path with this one.
1175
+ # (note that this might leave some stats in the parents
1176
+ # that is no longer justified by the leaves?)
1177
+ if isinstance(self._search_position, DetachedPathNode):
1178
+ return
1179
+ previous_node = self._search_position
1180
+ node = DetachedPathNode()
1181
+ if self.choices_made:
1182
+ self.choices_made[-1].replace_child(previous_node, node)
1183
+ self.choices_made.append(node)
1184
+ self._search_position = node.child
1185
+ self.raise_not_deterministic(
1186
+ previous_node,
1187
+ f"Expect to detach path at a stem node, not at this node: {previous_node}",
1188
+ currently_handling=currently_handling,
1189
+ )
1190
+ node = self.grow_into(DetachedPathNode())
1068
1191
  self.choices_made.append(node)
1069
1192
  self._search_position = node.child
1070
1193
  debug("Detached from search tree")
1071
1194
 
1072
1195
  def cap_result_at_unknown(self):
1196
+ # TODO: this doesn't seem to work as intended.
1197
+ # If any execution path is confirmed, the end result is sometimes confirmed as well.
1073
1198
  self.status_cap = VerificationStatus.UNKNOWN
1074
1199
 
1075
1200
  def bubble_status(
1076
1201
  self, analysis: CallAnalysis
1077
1202
  ) -> Tuple[Optional[CallAnalysis], bool]:
1078
1203
  # In some cases, we might ignore an attempt while not at a leaf.
1079
- if self._search_position.is_stem():
1080
- self._search_position = self._search_position.grow_into(
1081
- SearchLeaf(analysis)
1082
- )
1204
+ if isinstance(self._search_position, NodeStem):
1205
+ self._search_position = self.grow_into(SearchLeaf(analysis))
1083
1206
  else:
1084
- self._search_position = self._search_position.simplify()
1085
1207
  assert isinstance(self._search_position, SearchTreeNode)
1086
1208
  self._search_position.exhausted = True
1087
1209
  self._search_position.result = analysis
@@ -1090,11 +1212,12 @@ class StateSpace:
1090
1212
  return (analysis, True)
1091
1213
  for node in reversed(self.choices_made):
1092
1214
  node.update_result(analysis)
1093
- if in_debug():
1094
- for line in debug_path_tree(
1095
- self._root, set(self.choices_made + [self._search_position])
1096
- ):
1097
- debug(line)
1215
+ if False: # this is more noise than it's worth (usually)
1216
+ if in_debug():
1217
+ for line in debug_path_tree(
1218
+ self._root, set(self.choices_made + [self._search_position])
1219
+ ):
1220
+ debug(line)
1098
1221
  # debug('Path summary:', self.choices_made)
1099
1222
  first = self.choices_made[0]
1100
1223
  analysis = first.get_result()