crosshair-tool 0.0.95__cp311-cp311-musllinux_1_2_x86_64.whl → 0.0.97__cp311-cp311-musllinux_1_2_x86_64.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.

Potentially problematic release.


This version of crosshair-tool might be problematic. Click here for more details.

Files changed (46) hide show
  1. _crosshair_tracers.cpython-311-x86_64-linux-musl.so +0 -0
  2. crosshair/__init__.py +1 -1
  3. crosshair/_tracers_test.py +5 -5
  4. crosshair/codeconfig.py +3 -2
  5. crosshair/condition_parser.py +1 -0
  6. crosshair/condition_parser_test.py +0 -2
  7. crosshair/core.py +8 -9
  8. crosshair/core_test.py +2 -3
  9. crosshair/diff_behavior_test.py +0 -2
  10. crosshair/dynamic_typing.py +3 -3
  11. crosshair/enforce.py +1 -0
  12. crosshair/examples/check_examples_test.py +1 -0
  13. crosshair/fnutil.py +2 -3
  14. crosshair/fnutil_test.py +1 -4
  15. crosshair/fuzz_core_test.py +9 -1
  16. crosshair/libimpl/arraylib.py +1 -1
  17. crosshair/libimpl/builtinslib.py +77 -24
  18. crosshair/libimpl/builtinslib_ch_test.py +15 -5
  19. crosshair/libimpl/builtinslib_test.py +38 -1
  20. crosshair/libimpl/collectionslib_test.py +4 -4
  21. crosshair/libimpl/datetimelib.py +1 -3
  22. crosshair/libimpl/datetimelib_ch_test.py +5 -5
  23. crosshair/libimpl/encodings/_encutil.py +11 -6
  24. crosshair/libimpl/functoolslib.py +8 -2
  25. crosshair/libimpl/functoolslib_test.py +22 -6
  26. crosshair/libimpl/relib.py +1 -1
  27. crosshair/libimpl/unicodedatalib_test.py +3 -3
  28. crosshair/main.py +5 -3
  29. crosshair/opcode_intercept.py +45 -17
  30. crosshair/path_cover.py +5 -1
  31. crosshair/pathing_oracle.py +40 -3
  32. crosshair/pathing_oracle_test.py +21 -0
  33. crosshair/register_contract.py +1 -0
  34. crosshair/register_contract_test.py +2 -4
  35. crosshair/simplestructs.py +10 -8
  36. crosshair/statespace.py +74 -19
  37. crosshair/statespace_test.py +16 -0
  38. crosshair/tools/generate_demo_table.py +2 -2
  39. crosshair/tracers.py +8 -6
  40. crosshair/util.py +6 -6
  41. {crosshair_tool-0.0.95.dist-info → crosshair_tool-0.0.97.dist-info}/METADATA +4 -5
  42. {crosshair_tool-0.0.95.dist-info → crosshair_tool-0.0.97.dist-info}/RECORD +46 -45
  43. {crosshair_tool-0.0.95.dist-info → crosshair_tool-0.0.97.dist-info}/WHEEL +0 -0
  44. {crosshair_tool-0.0.95.dist-info → crosshair_tool-0.0.97.dist-info}/entry_points.txt +0 -0
  45. {crosshair_tool-0.0.95.dist-info → crosshair_tool-0.0.97.dist-info}/licenses/LICENSE +0 -0
  46. {crosshair_tool-0.0.95.dist-info → crosshair_tool-0.0.97.dist-info}/top_level.txt +0 -0
crosshair/__init__.py CHANGED
@@ -15,7 +15,7 @@ from crosshair.statespace import StateSpace
15
15
  from crosshair.tracers import NoTracing, ResumedTracing
16
16
  from crosshair.util import IgnoreAttempt, debug
17
17
 
18
- __version__ = "0.0.95" # Do not forget to update in setup.py!
18
+ __version__ = "0.0.97" # Do not forget to update in setup.py!
19
19
  __author__ = "Phillip Schanely"
20
20
  __license__ = "MIT"
21
21
  __status__ = "Alpha"
@@ -19,19 +19,19 @@ class ExampleModule:
19
19
 
20
20
  def test_CTracer_module_refcounts_dont_leak():
21
21
  mod = ExampleModule()
22
- assert sys.getrefcount(mod) == 2
22
+ base_count = sys.getrefcount(mod)
23
23
  tracer = CTracer()
24
24
  tracer.push_module(mod)
25
- assert sys.getrefcount(mod) == 3
25
+ assert sys.getrefcount(mod) == base_count + 1
26
26
  tracer.push_module(mod)
27
27
  tracer.start()
28
28
  tracer.stop()
29
- assert sys.getrefcount(mod) == 4
29
+ assert sys.getrefcount(mod) == base_count + 2
30
30
  tracer.pop_module(mod)
31
- assert sys.getrefcount(mod) == 3
31
+ assert sys.getrefcount(mod) == base_count + 1
32
32
  del tracer
33
33
  gc.collect()
34
- assert sys.getrefcount(mod) == 2
34
+ assert sys.getrefcount(mod) == base_count
35
35
 
36
36
 
37
37
  def _get_depths(fn):
crosshair/codeconfig.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Configure analysis options at different levels."""
2
+
2
3
  import importlib.resources
3
4
  import inspect
4
5
  import re
@@ -25,7 +26,7 @@ def get_directives(source_text: str) -> Iterable[Tuple[int, int, str]]:
25
26
  ret = []
26
27
  tokens = tokenize.generate_tokens(StringIO(source_text).readline)
27
28
  # TODO catch tokenize.TokenError ... just in case?
28
- for (toktyp, tokval, begin, _, _) in tokens:
29
+ for toktyp, tokval, begin, _, _ in tokens:
29
30
  linenum, colnum = begin
30
31
  if toktyp == tokenize.COMMENT:
31
32
  directive = _COMMENT_TOKEN_RE.sub(r"\1", tokval)
@@ -39,7 +40,7 @@ class InvalidDirective(Exception):
39
40
 
40
41
 
41
42
  def parse_directives(
42
- directive_lines: Iterable[Tuple[int, int, str]]
43
+ directive_lines: Iterable[Tuple[int, int, str]],
43
44
  ) -> AnalysisOptionSet:
44
45
  """
45
46
  Parse options from directives in comments.
@@ -520,6 +520,7 @@ class ConcreteConditionParser(ConditionParser):
520
520
  "__delattr__",
521
521
  "__replace__", # Will raise an exception with most arbitrary **kwargs.
522
522
  "__annotate__", # a staticmethod, but not isinstance(staticmethod)
523
+ "__annotate_func__",
523
524
  ):
524
525
  pass
525
526
  elif method_name == "__del__":
@@ -1,8 +1,6 @@
1
1
  import inspect
2
2
  import json
3
3
  import sys
4
- import textwrap
5
- import unittest
6
4
  from typing import List
7
5
 
8
6
  import pytest
crosshair/core.py CHANGED
@@ -377,7 +377,8 @@ def with_symbolic_self(symbolic_cls: Type, fn: Callable):
377
377
  elif any(isinstance(a, CrossHairValue) for a in args) or (
378
378
  kwargs and any(isinstance(a, CrossHairValue) for a in kwargs.values())
379
379
  ):
380
- self = symbolic_cls._smt_promote_literal(self)
380
+ # NOTE: _ch_create_from_literal is suppoerted for very few types right now
381
+ self = symbolic_cls._ch_create_from_literal(self)
381
382
  target_fn = getattr(symbolic_cls, fn.__name__)
382
383
  else:
383
384
  args = map(realize, args)
@@ -562,12 +563,12 @@ class SymbolicFactory:
562
563
  @overload
563
564
  def __call__(
564
565
  self, typ: Callable[..., _T], suffix: str = "", allow_subtypes: bool = True
565
- ) -> _T:
566
- ...
566
+ ) -> _T: ...
567
567
 
568
568
  @overload
569
- def __call__(self, typ: Any, suffix: str = "", allow_subtypes: bool = True) -> Any:
570
- ...
569
+ def __call__(
570
+ self, typ: Any, suffix: str = "", allow_subtypes: bool = True
571
+ ) -> Any: ...
571
572
 
572
573
  def __call__(self, typ, suffix: str = "", allow_subtypes: bool = True):
573
574
  """
@@ -652,8 +653,7 @@ def proxy_for_type(
652
653
  typ: Callable[..., _T],
653
654
  varname: str,
654
655
  allow_subtypes: bool = False,
655
- ) -> _T:
656
- ...
656
+ ) -> _T: ...
657
657
 
658
658
 
659
659
  @overload
@@ -661,8 +661,7 @@ def proxy_for_type(
661
661
  typ: Any,
662
662
  varname: str,
663
663
  allow_subtypes: bool = False,
664
- ) -> Any:
665
- ...
664
+ ) -> Any: ...
666
665
 
667
666
 
668
667
  def proxy_for_type(
crosshair/core_test.py CHANGED
@@ -5,7 +5,6 @@ import re
5
5
  import sys
6
6
  import time
7
7
  from typing import *
8
- from unittest import skipIf
9
8
 
10
9
  import pytest # type: ignore
11
10
 
@@ -736,7 +735,7 @@ def test_newtype() -> None:
736
735
  assert isinstance(x, SymbolicInt)
737
736
 
738
737
 
739
- @skipIf(sys.version_info < (3, 12), "type statements added in 3.12")
738
+ @pytest.mark.skipif(sys.version_info < (3, 12), reason="type statements added in 3.12")
740
739
  def test_type_statement() -> None:
741
740
  env: dict[str, Any] = {}
742
741
  exec("type MyIntNew = int\n", env)
@@ -747,7 +746,7 @@ def test_type_statement() -> None:
747
746
  assert isinstance(x, SymbolicInt)
748
747
 
749
748
 
750
- @skipIf(sys.version_info < (3, 12), "type statements added in 3.12")
749
+ @pytest.mark.skipif(sys.version_info < (3, 12), reason="type statements added in 3.12")
751
750
  def test_parameterized_type_statement() -> None:
752
751
  env: dict[str, Any] = {}
753
752
  exec("type Pair[A, B] = tuple[B, A]\n", env)
@@ -1,5 +1,3 @@
1
- import sys
2
- import unittest
3
1
  from typing import Callable, List, Optional
4
2
 
5
3
  from crosshair.diff_behavior import (
@@ -59,7 +59,7 @@ def unify_callable_args(
59
59
  return True
60
60
  if len(value_types) != len(recv_types):
61
61
  return False
62
- for (varg, rarg) in zip(value_types, recv_types):
62
+ for varg, rarg in zip(value_types, recv_types):
63
63
  # note reversal here: Callable is contravariant in argument types
64
64
  if not unify(rarg, varg, bindings):
65
65
  return False
@@ -206,7 +206,7 @@ def unify(
206
206
  vargs = [object for _ in rargs]
207
207
  else:
208
208
  return False
209
- for (varg, targ) in zip(vargs, rargs):
209
+ for varg, targ in zip(vargs, rargs):
210
210
  if not unify(varg, targ, bindings):
211
211
  return False
212
212
  return True
@@ -302,7 +302,7 @@ def intersect_signatures(
302
302
  is_squishy1 = var_pos1 is not None or var_key1 is not None
303
303
  is_squishy2 = var_pos2 is not None or var_key2 is not None
304
304
  out_params: Dict[str, Parameter] = {}
305
- for (p1, p2) in zip_longest(pos1, pos2):
305
+ for p1, p2 in zip_longest(pos1, pos2):
306
306
  if p1 is None:
307
307
  if is_squishy1:
308
308
  out_params[p2.name] = p2
crosshair/enforce.py CHANGED
@@ -36,6 +36,7 @@ def WithEnforcement(fn: Callable) -> Callable:
36
36
  Enforcement is normally disabled when calling from some internal files, for
37
37
  performance reasons. Use WithEnforcement to ensure it is enabled anywhere.
38
38
  """
39
+
39
40
  # This local function has a special name that we look for while tracing
40
41
  # (see the wants_codeobj method below):
41
42
  def _crosshair_with_enforcement(*a, **kw):
@@ -1,4 +1,5 @@
1
1
  """Run functional tests of the tool on all the examples."""
2
+
2
3
  import argparse
3
4
  import os
4
5
  import pathlib
crosshair/fnutil.py CHANGED
@@ -41,8 +41,7 @@ if sys.version_info >= (3, 8):
41
41
  from typing import Protocol
42
42
 
43
43
  class Descriptor(Protocol):
44
- def __get__(self, instance: object, cls: type) -> Any:
45
- ...
44
+ def __get__(self, instance: object, cls: type) -> Any: ...
46
45
 
47
46
  else:
48
47
  Descriptor = Any
@@ -344,7 +343,7 @@ def walk_paths(paths: Iterable[Path], ignore_missing=False) -> Iterable[Path]:
344
343
  else:
345
344
  raise FileNotFoundError(str(path))
346
345
  if path.is_dir():
347
- for (dirpath, _dirs, files) in os.walk(str(path)):
346
+ for dirpath, _dirs, files in os.walk(str(path)):
348
347
  for curfile in files:
349
348
  if analyzable_filename(curfile):
350
349
  yield Path(dirpath) / curfile
crosshair/fnutil_test.py CHANGED
@@ -26,10 +26,7 @@ def test_fn_globals_on_builtin() -> None:
26
26
 
27
27
  def test_resolve_signature_invalid_annotations() -> None:
28
28
  sig = resolve_signature(with_invalid_type_annotation)
29
- if sys.version_info >= (3, 14):
30
- assert sig == "TypeThatIsNotDefined"
31
- else:
32
- assert sig == "name 'TypeThatIsNotDefined' is not defined"
29
+ assert sig == "name 'TypeThatIsNotDefined' is not defined"
33
30
 
34
31
 
35
32
  @pytest.mark.skipif(
@@ -47,7 +47,7 @@ from crosshair.statespace import (
47
47
  from crosshair.stubs_parser import signature_from_stubs
48
48
  from crosshair.test_util import flexible_equal
49
49
  from crosshair.tracers import COMPOSITE_TRACER, NoTracing, ResumedTracing
50
- from crosshair.util import CrosshairUnsupported, debug, type_args_of
50
+ from crosshair.util import CrosshairUnsupported, debug, is_iterable, type_args_of
51
51
 
52
52
  FUZZ_SEED = 1348
53
53
 
@@ -267,6 +267,14 @@ class FuzzTester:
267
267
  ) -> object:
268
268
  for name in typed_args.keys():
269
269
  literal, symbolic = literal_args[name], symbolic_args[name]
270
+ with NoTracing():
271
+ # TODO: transition into general purpose SMT expr extractor
272
+ # for equality with constant
273
+ if hasattr(symbolic, "_smt_promote_literal"):
274
+ symbolic.var = symbolic._smt_promote_literal(literal) # type: ignore
275
+ elif is_iterable(literal) and is_iterable(symbolic):
276
+ with ResumedTracing():
277
+ space.add(len(literal) == len(symbolic)) # type: ignore
270
278
  if literal != symbolic:
271
279
  raise IgnoreAttempt(
272
280
  f'symbolic "{name}" not equal to literal "{name}"'
@@ -32,7 +32,7 @@ INT_TYPE_SIZE = {c: array(c).itemsize for c in INT_TYPE_BOUNDS.keys()}
32
32
 
33
33
  def pick_code(space: StateSpace) -> Tuple[str, int, int]:
34
34
  last_idx = len(INT_TYPE_BOUNDS) - 1
35
- for (idx, (code, rng)) in enumerate(INT_TYPE_BOUNDS.items()):
35
+ for idx, (code, rng) in enumerate(INT_TYPE_BOUNDS.items()):
36
36
  if idx < last_idx:
37
37
  if space.smt_fork(desc=f"not_{code}_array"):
38
38
  continue
@@ -189,6 +189,7 @@ def smt_not(x: object) -> Union[bool, "SymbolicBool"]:
189
189
 
190
190
  _NONHEAP_PYTYPES = set([int, float, bool, NoneType, complex])
191
191
 
192
+
192
193
  # TODO: isn't this pretty close to isinstance(typ, AtomicSymbolicValue)?
193
194
  def pytype_uses_heap(typ: Type) -> bool:
194
195
  return not (typ in _NONHEAP_PYTYPES)
@@ -346,7 +347,7 @@ class AtomicSymbolicValue(SymbolicValue):
346
347
  raise CrossHairInternal(f"_pytype not implemented in {cls}")
347
348
 
348
349
  @classmethod
349
- def _smt_promote_literal(cls, val: object) -> Optional[z3.SortRef]:
350
+ def _smt_promote_literal(cls, literal: object) -> Optional[z3.ExprRef]:
350
351
  raise CrossHairInternal(f"_smt_promote_literal not implemented in {cls}")
351
352
 
352
353
  @classmethod
@@ -900,6 +901,7 @@ def setup_binops():
900
901
  if isinstance(a, FiniteFloat)
901
902
  else a.var
902
903
  )
904
+ assert smt_a is not None
903
905
  if isinstance(b, NonFiniteFloat):
904
906
  if isnan(b.val):
905
907
  return (nan, nan)
@@ -912,6 +914,7 @@ def setup_binops():
912
914
  return (-1.0, b.val)
913
915
  else:
914
916
  return (0.0, a.val if isinstance(a, FiniteFloat) else a)
917
+ assert smt_b is not None
915
918
 
916
919
  remainder = z3.Real(f"remainder{space.uniq()}")
917
920
  modproduct = z3.Int(f"modproduct{space.uniq()}")
@@ -1120,7 +1123,7 @@ class SymbolicBool(SymbolicIntable, AtomicSymbolicValue):
1120
1123
  return bool
1121
1124
 
1122
1125
  @classmethod
1123
- def _smt_promote_literal(cls, literal) -> Optional[z3.SortRef]:
1126
+ def _smt_promote_literal(cls, literal) -> Optional[z3.ExprRef]:
1124
1127
  if isinstance(literal, bool):
1125
1128
  return z3.BoolVal(literal)
1126
1129
  return None
@@ -1189,7 +1192,7 @@ class SymbolicInt(SymbolicIntable, AtomicSymbolicValue):
1189
1192
  return int
1190
1193
 
1191
1194
  @classmethod
1192
- def _smt_promote_literal(cls, literal) -> Optional[z3.SortRef]:
1195
+ def _smt_promote_literal(cls, literal) -> Optional[z3.ExprRef]:
1193
1196
  if isinstance(literal, int):
1194
1197
  return z3IntVal(literal)
1195
1198
  return None
@@ -1410,7 +1413,7 @@ class PreciseIeeeSymbolicFloat(SymbolicFloat):
1410
1413
  return _PRECISE_IEEE_FLOAT_SORT
1411
1414
 
1412
1415
  @classmethod
1413
- def _smt_promote_literal(cls, literal) -> Optional[z3.SortRef]:
1416
+ def _smt_promote_literal(cls, literal) -> Optional[z3.ExprRef]:
1414
1417
  if isinstance(literal, float):
1415
1418
  return z3.FPVal(literal, cls._ch_smt_sort())
1416
1419
  return None
@@ -1533,7 +1536,7 @@ class RealBasedSymbolicFloat(SymbolicFloat):
1533
1536
  return z3.RealSort()
1534
1537
 
1535
1538
  @classmethod
1536
- def _smt_promote_literal(cls, literal) -> Optional[z3.SortRef]:
1539
+ def _smt_promote_literal(cls, literal) -> Optional[z3.ExprRef]:
1537
1540
  if isinstance(literal, float) and isfinite(literal):
1538
1541
  return z3.RealVal(literal)
1539
1542
  return None
@@ -1591,6 +1594,7 @@ class RealBasedSymbolicFloat(SymbolicFloat):
1591
1594
  denominator = SymbolicInt("denominator" + space.uniq())
1592
1595
  space.add(denominator.var > 0)
1593
1596
  space.add(numerator.var == denominator.var * self.var)
1597
+
1594
1598
  # There are many valid integer ratios to return. Experimentally, both
1595
1599
  # z3 and CPython tend to pick the same ones. But verify this, while
1596
1600
  # deferring materialization:
@@ -2330,7 +2334,7 @@ class SymbolicRange:
2330
2334
  return False
2331
2335
  if len(self) != len(other):
2332
2336
  return False
2333
- for (v1, v2) in zip(self, other):
2337
+ for v1, v2 in zip(self, other):
2334
2338
  if v1 != v2:
2335
2339
  return False
2336
2340
  return True
@@ -2447,7 +2451,7 @@ class SymbolicType(AtomicSymbolicValue, SymbolicValue, Untracable):
2447
2451
  return type
2448
2452
 
2449
2453
  @classmethod
2450
- def _smt_promote_literal(cls, literal) -> Optional[z3.SortRef]:
2454
+ def _smt_promote_literal(cls, literal) -> Optional[z3.ExprRef]:
2451
2455
  if isinstance(literal, type):
2452
2456
  return context_statespace().extra(SymbolicTypeRepository).get_type(literal)
2453
2457
  return None
@@ -2668,10 +2672,25 @@ class SymbolicCallable:
2668
2672
  __annotations__: dict = {}
2669
2673
 
2670
2674
  def __init__(self, values: list):
2675
+ """
2676
+ A function that will ignore its arguments and produce return values
2677
+ from the list given.
2678
+ If the given list is exhausted, the function will just repeatedly
2679
+ return the final value in the list.
2680
+
2681
+ If `values` is concrete, it must be non-mepty.
2682
+ If `values` is a symbolic list, it will be forced to be non-empty
2683
+ (the caller must enure that's possible).
2684
+ """
2671
2685
  assert not is_tracing()
2672
2686
  with ResumedTracing():
2673
- if not values:
2674
- raise IgnoreAttempt
2687
+ has_values = len(values) > 0
2688
+ if isinstance(values, CrossHairValue):
2689
+ space = context_statespace()
2690
+ assert space.is_possible(has_values)
2691
+ space.add(has_values)
2692
+ else:
2693
+ assert has_values
2675
2694
  self.values = values
2676
2695
  self.idx = 0
2677
2696
 
@@ -2695,6 +2714,7 @@ class SymbolicCallable:
2695
2714
  if idx >= len(values):
2696
2715
  return values[-1]
2697
2716
  else:
2717
+ self.idx += 1
2698
2718
  return values[idx]
2699
2719
 
2700
2720
  def __bool__(self):
@@ -2767,7 +2787,7 @@ class SymbolicBoundedIntTuple(collections.abc.Sequence):
2767
2787
  with NoTracing():
2768
2788
  self._create_up_to(realize(otherlen))
2769
2789
  constraints = []
2770
- for (int1, int2) in zip(self._created_vars, tracing_iter(other)):
2790
+ for int1, int2 in zip(self._created_vars, tracing_iter(other)):
2771
2791
  smtint2 = force_to_smt_sort(int2, SymbolicInt)
2772
2792
  constraints.append(int1.var == smtint2)
2773
2793
  return SymbolicBool(z3.And(*constraints))
@@ -2894,7 +2914,7 @@ class AnySymbolicStr(AbcString):
2894
2914
  raise TypeError
2895
2915
  if self == other:
2896
2916
  return True if op in (ops.le, ops.ge) else False
2897
- for (mych, otherch) in zip_longest(iter(self), iter(other)):
2917
+ for mych, otherch in zip_longest(iter(self), iter(other)):
2898
2918
  if mych == otherch:
2899
2919
  continue
2900
2920
  if mych is None:
@@ -3156,7 +3176,7 @@ class AnySymbolicStr(AbcString):
3156
3176
 
3157
3177
  else:
3158
3178
  raise TypeError
3159
- for (idx, ch) in enumerate(self):
3179
+ for idx, ch in enumerate(self):
3160
3180
  if not filter(ch):
3161
3181
  return self[idx:]
3162
3182
  return ""
@@ -3168,7 +3188,7 @@ class AnySymbolicStr(AbcString):
3168
3188
  mylen = self.__len__()
3169
3189
  if mylen == 0:
3170
3190
  return []
3171
- for (idx, ch) in enumerate(self):
3191
+ for idx, ch in enumerate(self):
3172
3192
  codepoint = ord(ch)
3173
3193
  with NoTracing():
3174
3194
  space = context_statespace()
@@ -3406,6 +3426,26 @@ class AnySymbolicStr(AbcString):
3406
3426
  return "0" * fill_length + self
3407
3427
 
3408
3428
 
3429
+ def _unfindable_range(start: Optional[int], end: Optional[int], mylen: int) -> bool:
3430
+ """
3431
+ Emulates some preliminary checks that CPython makes before searching
3432
+ for substrings within some bounds. (in e.g. str.find, str.startswith, etc)
3433
+ """
3434
+ if start is None or start == 0 or start <= -mylen:
3435
+ return False
3436
+
3437
+ # At this point, we know that `start` is defined and points to an index after 0
3438
+ if end is None or end >= mylen:
3439
+ return start > mylen
3440
+
3441
+ # At this point, we know that `end` is defined and points to an index before the end of the string
3442
+ if start < 0:
3443
+ start += mylen
3444
+ if end < 0:
3445
+ end += mylen
3446
+ return end < start
3447
+
3448
+
3409
3449
  class LazyIntSymbolicStr(AnySymbolicStr, CrossHairValue):
3410
3450
  """
3411
3451
  A symbolic string that lazily generates SymbolicInt-based characters as needed.
@@ -3444,10 +3484,8 @@ class LazyIntSymbolicStr(AnySymbolicStr, CrossHairValue):
3444
3484
  codepoints = tuple(self._codepoints)
3445
3485
  return "".join(chr(realize(x)) for x in codepoints)
3446
3486
 
3447
- # This is normally an AtomicSymbolicValue method, but sometimes it's used in a
3448
- # duck-typing way.
3449
3487
  @classmethod
3450
- def _smt_promote_literal(cls, val: object) -> Optional[z3.SortRef]:
3488
+ def _ch_create_from_literal(cls, val: object) -> Optional[CrossHairValue]:
3451
3489
  if isinstance(val, str):
3452
3490
  return LazyIntSymbolicStr(list(map(ord, val)))
3453
3491
  return None
@@ -3481,6 +3519,10 @@ class LazyIntSymbolicStr(AnySymbolicStr, CrossHairValue):
3481
3519
  with NoTracing():
3482
3520
  if not isinstance(i, (Integral, slice)):
3483
3521
  raise TypeError(type(i))
3522
+ # This could/should? be symbolic by naming all the possibilities.
3523
+ # Note the slice case still must realize the return length.
3524
+ # Especially because we no longer explore realization trees except
3525
+ # as a last resort.
3484
3526
  i = deep_realize(i)
3485
3527
  with ResumedTracing():
3486
3528
  newcontents = self._codepoints[i]
@@ -3561,11 +3603,15 @@ class LazyIntSymbolicStr(AnySymbolicStr, CrossHairValue):
3561
3603
  return any(self.endswith(s, start, end) for s in substr)
3562
3604
  if not isinstance(substr, str):
3563
3605
  raise TypeError
3606
+ substrlen = len(substr)
3564
3607
  if start is None and end is None:
3565
3608
  matchable = self
3566
3609
  else:
3567
3610
  matchable = self[start:end]
3568
- return matchable[-len(substr) :] == substr
3611
+ if substrlen == 0:
3612
+ return not _unfindable_range(start, end, len(self))
3613
+ else:
3614
+ return matchable[-substrlen:] == substr
3569
3615
 
3570
3616
  def startswith(self, substr, start=None, end=None):
3571
3617
  if isinstance(substr, tuple):
@@ -3575,6 +3621,10 @@ class LazyIntSymbolicStr(AnySymbolicStr, CrossHairValue):
3575
3621
  if start is None and end is None:
3576
3622
  matchable = self
3577
3623
  else:
3624
+ # Wacky special case: the empty string is findable off the left
3625
+ # side but not the right!
3626
+ if _unfindable_range(start, end, len(self)):
3627
+ return False
3578
3628
  matchable = self[start:end]
3579
3629
  return matchable[: len(substr)] == substr
3580
3630
 
@@ -3615,7 +3665,7 @@ class LazyIntSymbolicStr(AnySymbolicStr, CrossHairValue):
3615
3665
  end += mylen
3616
3666
  matchstr = self[start:end] if start != 0 or end is not mylen else self
3617
3667
  if len(substr) == 0:
3618
- # Add oddity of CPython. We can find the empty string when over-slicing
3668
+ # An oddity of CPython. We can find the empty string when over-slicing
3619
3669
  # off the left side of the string, but not off the right:
3620
3670
  # ''.find('', 3, 4) == -1
3621
3671
  # ''.find('', -4, -3) == 0
@@ -3962,7 +4012,7 @@ class SymbolicByteArray(BytesLike, ShellMutableSequence): # type: ignore
3962
4012
 
3963
4013
 
3964
4014
  class SymbolicMemoryView(BytesLike):
3965
- format = "B"
4015
+ format = "B" # type: ignore
3966
4016
  itemsize = 1
3967
4017
  ndim = 1
3968
4018
  strides = (1,)
@@ -4771,11 +4821,11 @@ def _str_percent_format(self, other):
4771
4821
  return self.__mod__(deep_realize(other))
4772
4822
 
4773
4823
 
4774
- def _bytes_join(self, itr) -> str:
4824
+ def _bytes_join(self, itr) -> bytes:
4775
4825
  return _join(self, itr, self_type=bytes, item_type=Buffer)
4776
4826
 
4777
4827
 
4778
- def _bytearray_join(self, itr) -> str:
4828
+ def _bytearray_join(self, itr) -> bytes:
4779
4829
  return _join(self, itr, self_type=bytearray, item_type=Buffer)
4780
4830
 
4781
4831
 
@@ -4790,14 +4840,17 @@ def _str_format_map(self, map) -> Union[AnySymbolicStr, str]:
4790
4840
 
4791
4841
 
4792
4842
  def _str_startswith(self, substr, start=None, end=None) -> bool:
4793
- if not isinstance(self, str):
4794
- raise TypeError
4795
4843
  with NoTracing():
4844
+ if isinstance(self, LazyIntSymbolicStr):
4845
+ with ResumedTracing():
4846
+ return self.startswith(substr, start, end)
4847
+ elif not isinstance(self, str):
4848
+ raise TypeError
4796
4849
  # Handle native values with native implementation:
4797
4850
  if type(substr) is str:
4798
4851
  return self.startswith(substr, start, end)
4799
4852
  if type(substr) is tuple:
4800
- if all(type(i) is str for i in substr):
4853
+ if all(type(s) is str for s in substr):
4801
4854
  return self.startswith(substr, start, end)
4802
4855
  symbolic_self = LazyIntSymbolicStr([ord(c) for c in self])
4803
4856
  return symbolic_self.startswith(substr, start, end)
@@ -178,7 +178,7 @@ def check_iter(obj: Union[str, List[int], Dict[int, int]]) -> ResultComparison:
178
178
 
179
179
 
180
180
  def check_len(
181
- s: Union[Dict[int, int], Tuple[int, ...], str, List[int], Set[int]]
181
+ s: Union[Dict[int, int], Tuple[int, ...], str, List[int], Set[int]],
182
182
  ) -> ResultComparison:
183
183
  """post: _"""
184
184
  return compare_results(len, s)
@@ -606,9 +606,14 @@ def check_str_endswith(
606
606
  string: str, suffix: str, start: Optional[int], end: Optional[int]
607
607
  ) -> ResultComparison:
608
608
  """post: _"""
609
+ # crosshair: max_uninteresting_iterations=100
610
+
611
+ for i in (len(string), len(suffix), start, end):
612
+ if i is not None and abs(i) >= 1:
613
+ pass
614
+
609
615
  return compare_results(
610
- lambda s, *a: s.endswith(*a),
611
- string,
616
+ lambda s, *a, **kw: s.endswith(*a, **kw), string, suffix, start, end
612
617
  )
613
618
 
614
619
 
@@ -825,6 +830,11 @@ def check_str_startswith(
825
830
  end: Optional[int],
826
831
  ) -> ResultComparison:
827
832
  """post: _"""
833
+ # crosshair: max_uninteresting_iterations=100
834
+
835
+ for i in (len(string), len(prefix), start, end):
836
+ if i is not None and abs(i) >= 1:
837
+ pass
828
838
  return compare_results(
829
839
  lambda s, *a, **kw: s.startswith(*a, **kw), string, prefix, start, end
830
840
  )
@@ -974,7 +984,7 @@ def check_bytes___init__(source: Union[int, List[int], bytes, bytearray, memoryv
974
984
 
975
985
 
976
986
  def check_bytearray___init__(
977
- source: Union[int, List[int], bytes, bytearray, memoryview]
987
+ source: Union[int, List[int], bytes, bytearray, memoryview],
978
988
  ):
979
989
  """
980
990
  post: _
@@ -984,7 +994,7 @@ def check_bytearray___init__(
984
994
 
985
995
 
986
996
  def check_memoryview___init__(
987
- source: Union[int, List[int], bytes, bytearray, memoryview]
997
+ source: Union[int, List[int], bytes, bytearray, memoryview],
988
998
  ):
989
999
  """
990
1000
  post: _