crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_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.
Files changed (176) hide show
  1. _crosshair_tracers.cpython-312-darwin.so +0 -0
  2. crosshair/__init__.py +42 -0
  3. crosshair/__main__.py +8 -0
  4. crosshair/_mark_stacks.h +790 -0
  5. crosshair/_preliminaries_test.py +18 -0
  6. crosshair/_tracers.h +94 -0
  7. crosshair/_tracers_pycompat.h +522 -0
  8. crosshair/_tracers_test.py +138 -0
  9. crosshair/abcstring.py +245 -0
  10. crosshair/auditwall.py +190 -0
  11. crosshair/auditwall_test.py +77 -0
  12. crosshair/codeconfig.py +113 -0
  13. crosshair/codeconfig_test.py +117 -0
  14. crosshair/condition_parser.py +1237 -0
  15. crosshair/condition_parser_test.py +497 -0
  16. crosshair/conftest.py +30 -0
  17. crosshair/copyext.py +155 -0
  18. crosshair/copyext_test.py +84 -0
  19. crosshair/core.py +1763 -0
  20. crosshair/core_and_libs.py +149 -0
  21. crosshair/core_regestered_types_test.py +82 -0
  22. crosshair/core_test.py +1316 -0
  23. crosshair/diff_behavior.py +314 -0
  24. crosshair/diff_behavior_test.py +261 -0
  25. crosshair/dynamic_typing.py +346 -0
  26. crosshair/dynamic_typing_test.py +210 -0
  27. crosshair/enforce.py +282 -0
  28. crosshair/enforce_test.py +182 -0
  29. crosshair/examples/PEP316/__init__.py +1 -0
  30. crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
  31. crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
  32. crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
  33. crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
  34. crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
  35. crosshair/examples/PEP316/correct_code/__init__.py +0 -0
  36. crosshair/examples/PEP316/correct_code/arith.py +60 -0
  37. crosshair/examples/PEP316/correct_code/chess.py +77 -0
  38. crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
  39. crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
  40. crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
  41. crosshair/examples/PEP316/correct_code/showcase.py +104 -0
  42. crosshair/examples/__init__.py +0 -0
  43. crosshair/examples/check_examples_test.py +146 -0
  44. crosshair/examples/deal/__init__.py +1 -0
  45. crosshair/examples/icontract/__init__.py +1 -0
  46. crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
  47. crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
  48. crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
  49. crosshair/examples/icontract/correct_code/__init__.py +0 -0
  50. crosshair/examples/icontract/correct_code/arith.py +51 -0
  51. crosshair/examples/icontract/correct_code/showcase.py +94 -0
  52. crosshair/fnutil.py +391 -0
  53. crosshair/fnutil_test.py +75 -0
  54. crosshair/fuzz_core_test.py +516 -0
  55. crosshair/libimpl/__init__.py +0 -0
  56. crosshair/libimpl/arraylib.py +161 -0
  57. crosshair/libimpl/binascii_ch_test.py +30 -0
  58. crosshair/libimpl/binascii_test.py +67 -0
  59. crosshair/libimpl/binasciilib.py +150 -0
  60. crosshair/libimpl/bisectlib_test.py +23 -0
  61. crosshair/libimpl/builtinslib.py +5228 -0
  62. crosshair/libimpl/builtinslib_ch_test.py +1191 -0
  63. crosshair/libimpl/builtinslib_test.py +3735 -0
  64. crosshair/libimpl/codecslib.py +86 -0
  65. crosshair/libimpl/codecslib_test.py +86 -0
  66. crosshair/libimpl/collectionslib.py +264 -0
  67. crosshair/libimpl/collectionslib_ch_test.py +252 -0
  68. crosshair/libimpl/collectionslib_test.py +332 -0
  69. crosshair/libimpl/copylib.py +23 -0
  70. crosshair/libimpl/copylib_test.py +18 -0
  71. crosshair/libimpl/datetimelib.py +2559 -0
  72. crosshair/libimpl/datetimelib_ch_test.py +354 -0
  73. crosshair/libimpl/datetimelib_test.py +112 -0
  74. crosshair/libimpl/decimallib.py +5257 -0
  75. crosshair/libimpl/decimallib_ch_test.py +78 -0
  76. crosshair/libimpl/decimallib_test.py +76 -0
  77. crosshair/libimpl/encodings/__init__.py +23 -0
  78. crosshair/libimpl/encodings/_encutil.py +187 -0
  79. crosshair/libimpl/encodings/ascii.py +44 -0
  80. crosshair/libimpl/encodings/latin_1.py +40 -0
  81. crosshair/libimpl/encodings/utf_8.py +93 -0
  82. crosshair/libimpl/encodings_ch_test.py +83 -0
  83. crosshair/libimpl/fractionlib.py +16 -0
  84. crosshair/libimpl/fractionlib_test.py +80 -0
  85. crosshair/libimpl/functoolslib.py +34 -0
  86. crosshair/libimpl/functoolslib_test.py +56 -0
  87. crosshair/libimpl/hashliblib.py +30 -0
  88. crosshair/libimpl/hashliblib_test.py +18 -0
  89. crosshair/libimpl/heapqlib.py +47 -0
  90. crosshair/libimpl/heapqlib_test.py +21 -0
  91. crosshair/libimpl/importliblib.py +18 -0
  92. crosshair/libimpl/importliblib_test.py +38 -0
  93. crosshair/libimpl/iolib.py +216 -0
  94. crosshair/libimpl/iolib_ch_test.py +128 -0
  95. crosshair/libimpl/iolib_test.py +19 -0
  96. crosshair/libimpl/ipaddresslib.py +8 -0
  97. crosshair/libimpl/itertoolslib.py +44 -0
  98. crosshair/libimpl/itertoolslib_test.py +44 -0
  99. crosshair/libimpl/jsonlib.py +984 -0
  100. crosshair/libimpl/jsonlib_ch_test.py +42 -0
  101. crosshair/libimpl/jsonlib_test.py +51 -0
  102. crosshair/libimpl/mathlib.py +179 -0
  103. crosshair/libimpl/mathlib_ch_test.py +44 -0
  104. crosshair/libimpl/mathlib_test.py +67 -0
  105. crosshair/libimpl/oslib.py +7 -0
  106. crosshair/libimpl/pathliblib_test.py +10 -0
  107. crosshair/libimpl/randomlib.py +178 -0
  108. crosshair/libimpl/randomlib_test.py +120 -0
  109. crosshair/libimpl/relib.py +846 -0
  110. crosshair/libimpl/relib_ch_test.py +169 -0
  111. crosshair/libimpl/relib_test.py +493 -0
  112. crosshair/libimpl/timelib.py +72 -0
  113. crosshair/libimpl/timelib_test.py +82 -0
  114. crosshair/libimpl/typeslib.py +15 -0
  115. crosshair/libimpl/typeslib_test.py +36 -0
  116. crosshair/libimpl/unicodedatalib.py +75 -0
  117. crosshair/libimpl/unicodedatalib_test.py +42 -0
  118. crosshair/libimpl/urlliblib.py +23 -0
  119. crosshair/libimpl/urlliblib_test.py +19 -0
  120. crosshair/libimpl/weakreflib.py +13 -0
  121. crosshair/libimpl/weakreflib_test.py +69 -0
  122. crosshair/libimpl/zliblib.py +15 -0
  123. crosshair/libimpl/zliblib_test.py +13 -0
  124. crosshair/lsp_server.py +261 -0
  125. crosshair/lsp_server_test.py +30 -0
  126. crosshair/main.py +973 -0
  127. crosshair/main_test.py +543 -0
  128. crosshair/objectproxy.py +376 -0
  129. crosshair/objectproxy_test.py +41 -0
  130. crosshair/opcode_intercept.py +601 -0
  131. crosshair/opcode_intercept_test.py +304 -0
  132. crosshair/options.py +218 -0
  133. crosshair/options_test.py +10 -0
  134. crosshair/patch_equivalence_test.py +75 -0
  135. crosshair/path_cover.py +209 -0
  136. crosshair/path_cover_test.py +138 -0
  137. crosshair/path_search.py +161 -0
  138. crosshair/path_search_test.py +52 -0
  139. crosshair/pathing_oracle.py +271 -0
  140. crosshair/pathing_oracle_test.py +21 -0
  141. crosshair/pure_importer.py +27 -0
  142. crosshair/pure_importer_test.py +16 -0
  143. crosshair/py.typed +0 -0
  144. crosshair/register_contract.py +273 -0
  145. crosshair/register_contract_test.py +190 -0
  146. crosshair/simplestructs.py +1165 -0
  147. crosshair/simplestructs_test.py +283 -0
  148. crosshair/smtlib.py +24 -0
  149. crosshair/smtlib_test.py +14 -0
  150. crosshair/statespace.py +1199 -0
  151. crosshair/statespace_test.py +108 -0
  152. crosshair/stubs_parser.py +352 -0
  153. crosshair/stubs_parser_test.py +43 -0
  154. crosshair/test_util.py +329 -0
  155. crosshair/test_util_test.py +26 -0
  156. crosshair/tools/__init__.py +0 -0
  157. crosshair/tools/check_help_in_doc.py +264 -0
  158. crosshair/tools/check_init_and_setup_coincide.py +119 -0
  159. crosshair/tools/generate_demo_table.py +127 -0
  160. crosshair/tracers.py +544 -0
  161. crosshair/tracers_test.py +154 -0
  162. crosshair/type_repo.py +151 -0
  163. crosshair/unicode_categories.py +589 -0
  164. crosshair/unicode_categories_test.py +27 -0
  165. crosshair/util.py +741 -0
  166. crosshair/util_test.py +173 -0
  167. crosshair/watcher.py +307 -0
  168. crosshair/watcher_test.py +107 -0
  169. crosshair/z3util.py +76 -0
  170. crosshair/z3util_test.py +11 -0
  171. crosshair_tool-0.0.99.dist-info/METADATA +144 -0
  172. crosshair_tool-0.0.99.dist-info/RECORD +176 -0
  173. crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
  174. crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
  175. crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
  176. crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
crosshair/main.py ADDED
@@ -0,0 +1,973 @@
1
+ import argparse
2
+ import enum
3
+ import linecache
4
+ import os.path
5
+ import random
6
+ import shutil
7
+ import sys
8
+ import textwrap
9
+ import time
10
+ import traceback
11
+ from collections import deque
12
+ from pathlib import Path
13
+ from types import ModuleType
14
+ from typing import (
15
+ Callable,
16
+ Counter,
17
+ Dict,
18
+ Iterable,
19
+ List,
20
+ MutableMapping,
21
+ NoReturn,
22
+ Optional,
23
+ Sequence,
24
+ TextIO,
25
+ Tuple,
26
+ Union,
27
+ cast,
28
+ )
29
+
30
+ from crosshair import env_info
31
+ from crosshair.auditwall import disable_auditwall, engage_auditwall
32
+ from crosshair.core_and_libs import (
33
+ AnalysisMessage,
34
+ MessageType,
35
+ analyze_any,
36
+ installed_plugins,
37
+ run_checkables,
38
+ )
39
+ from crosshair.diff_behavior import ExceptionEquivalenceType, diff_behavior
40
+ from crosshair.fnutil import (
41
+ FUNCTIONINFO_DESCRIPTOR_TYPES,
42
+ FunctionInfo,
43
+ get_top_level_classes_and_functions,
44
+ load_files_or_qualnames,
45
+ )
46
+ from crosshair.options import (
47
+ DEFAULT_OPTIONS,
48
+ AnalysisKind,
49
+ AnalysisOptions,
50
+ AnalysisOptionSet,
51
+ option_set_from_dict,
52
+ )
53
+ from crosshair.path_cover import (
54
+ CoverageType,
55
+ output_argument_dictionary_paths,
56
+ output_eval_exression_paths,
57
+ output_pytest_paths,
58
+ path_cover,
59
+ )
60
+ from crosshair.path_search import OptimizationKind, path_search
61
+ from crosshair.pure_importer import prefer_pure_python_imports
62
+ from crosshair.register_contract import REGISTERED_CONTRACTS
63
+ from crosshair.util import (
64
+ ErrorDuringImport,
65
+ NotDeterministic,
66
+ add_to_pypath,
67
+ debug,
68
+ format_boundargs,
69
+ format_boundargs_as_dictionary,
70
+ in_debug,
71
+ set_debug,
72
+ )
73
+ from crosshair.watcher import Watcher
74
+
75
+
76
+ class ExampleOutputFormat(enum.Enum):
77
+ ARGUMENT_DICTIONARY = "ARGUMENT_DICTIONARY" # deprecated
78
+ ARG_DICTIONARY = "ARG_DICTIONARY"
79
+ EVAL_EXPRESSION = "EVAL_EXPRESSION"
80
+ PYTEST = "PYTEST"
81
+
82
+
83
+ def analysis_kind(argstr: str) -> Sequence[AnalysisKind]:
84
+ try:
85
+ ret = [AnalysisKind[part.strip()] for part in argstr.split(",")]
86
+ except KeyError:
87
+ raise ValueError
88
+ return ret
89
+
90
+
91
+ def command_line_parser() -> argparse.ArgumentParser:
92
+ common = argparse.ArgumentParser(
93
+ add_help=False, formatter_class=argparse.RawTextHelpFormatter
94
+ )
95
+ common.add_argument(
96
+ "--verbose",
97
+ "-v",
98
+ action="store_true",
99
+ help="Output additional debugging information on stderr",
100
+ )
101
+ common.add_argument(
102
+ "--extra_plugin",
103
+ type=str,
104
+ nargs="+",
105
+ help="Plugin file(s) you wish to use during the current execution",
106
+ )
107
+ parser = argparse.ArgumentParser(
108
+ prog="crosshair", description="CrossHair Analysis Tool"
109
+ )
110
+ subparsers = parser.add_subparsers(help="sub-command help", dest="action")
111
+ check_parser = subparsers.add_parser(
112
+ "check",
113
+ help="Analyze a file or function",
114
+ parents=[common],
115
+ formatter_class=argparse.RawTextHelpFormatter,
116
+ description=textwrap.dedent(
117
+ """\
118
+ The check command looks for counterexamples that break contracts.
119
+
120
+ It outputs machine-readable messages in this format on stdout:
121
+ <filename>:<line number>: error: <error message>
122
+
123
+ It exits with one of the following codes:
124
+ 0 : No counterexamples are found
125
+ 1 : Counterexample(s) have been found
126
+ 2 : Other error
127
+ """
128
+ ),
129
+ )
130
+ check_parser.add_argument(
131
+ "--report_all",
132
+ action="store_true",
133
+ help="Output analysis results for all postconditions (not just failing ones)",
134
+ )
135
+ check_parser.add_argument(
136
+ "--report_verbose",
137
+ dest="report_verbose",
138
+ action="store_true",
139
+ help="Output context and stack traces for counterexamples",
140
+ )
141
+ check_parser.add_argument(
142
+ "target",
143
+ metavar="TARGET",
144
+ type=str,
145
+ nargs="+",
146
+ help=textwrap.dedent(
147
+ """\
148
+ A fully qualified module, class, or function, or
149
+ a directory (which will be recursively analyzed), or
150
+ a file path with an optional ":<line-number>" suffix.
151
+ See https://crosshair.readthedocs.io/en/latest/contracts.html#targeting
152
+ """
153
+ ),
154
+ )
155
+ search_parser = subparsers.add_parser(
156
+ "search",
157
+ help="Find arguments to make a function complete without error",
158
+ parents=[common],
159
+ formatter_class=argparse.RawTextHelpFormatter,
160
+ description=textwrap.dedent(
161
+ """\
162
+ The search command finds arguments for a function that causes it to
163
+ complete without error.
164
+
165
+ Results (if any) are written to stdout in the form of a repr'd
166
+ dictionary, mapping argument names to values.
167
+ """
168
+ ),
169
+ )
170
+ search_parser.add_argument(
171
+ "fn",
172
+ metavar="FUNCTION",
173
+ type=str,
174
+ help='A fully-qualified function to explore (e.g. "mymodule.myfunc")',
175
+ )
176
+ search_parser.add_argument(
177
+ "--optimization",
178
+ type=lambda e: OptimizationKind[e.upper()], # type: ignore
179
+ choices=OptimizationKind.__members__.values(),
180
+ metavar="OPTIMIZATION_TYPE",
181
+ default=OptimizationKind.SIMPLIFY,
182
+ help=textwrap.dedent(
183
+ """\
184
+ Controls what kind of arguments are produced.
185
+ Optimization effectiveness will vary wildly depnding on the nature of
186
+ the function.
187
+ simplify : [default] Attempt to minimize the size
188
+ (in characters) of the arguments.
189
+ none : Output the first set of arguments found.
190
+ minimize_int : Attempt to minimize an integer returned by the
191
+ function. Negative return values are ignored.
192
+ """
193
+ ),
194
+ )
195
+ search_parser.add_argument(
196
+ "--output_all_examples",
197
+ action="store_true",
198
+ default=False,
199
+ help=textwrap.dedent(
200
+ """\
201
+ When optimizing, output an example every time a new best score is discovered.
202
+ """
203
+ ),
204
+ )
205
+ search_parser.add_argument(
206
+ "--argument_formatter",
207
+ metavar="FUNCTION",
208
+ type=str,
209
+ help=textwrap.dedent(
210
+ """\
211
+ The (fully-qualified) name of a function for formatting produced
212
+ arguments. If specified, crosshair will call this function instead of
213
+ repr() when printing arguments to stdout.
214
+ Your formatting function will be pased an `inspect.BoundArguments`
215
+ instance. It should return a string.
216
+ """
217
+ ),
218
+ )
219
+
220
+ watch_parser = subparsers.add_parser(
221
+ "watch",
222
+ help="Continuously watch and analyze a directory",
223
+ parents=[common],
224
+ formatter_class=argparse.RawTextHelpFormatter,
225
+ description=textwrap.dedent(
226
+ """\
227
+ The watch command continuously looks for contract counterexamples.
228
+ Type Ctrl-C to stop this command.
229
+ """
230
+ ),
231
+ )
232
+ watch_parser.add_argument(
233
+ "directory",
234
+ metavar="TARGET",
235
+ type=str,
236
+ nargs="+",
237
+ help=textwrap.dedent(
238
+ """\
239
+ File or directory to watch. Directories will be recursively analyzed.
240
+ See https://crosshair.readthedocs.io/en/latest/contracts.html#targeting
241
+ """
242
+ ),
243
+ )
244
+ diffbehavior_parser = subparsers.add_parser(
245
+ "diffbehavior",
246
+ formatter_class=argparse.RawTextHelpFormatter,
247
+ help="Find differences in the behavior of two functions",
248
+ description=textwrap.dedent(
249
+ """\
250
+ Find differences in the behavior of two functions.
251
+ See https://crosshair.readthedocs.io/en/latest/diff_behavior.html
252
+ """
253
+ ),
254
+ parents=[common],
255
+ )
256
+ diffbehavior_parser.add_argument(
257
+ "fn1",
258
+ metavar="FUNCTION1",
259
+ type=str,
260
+ help='first fully-qualified function to compare (e.g. "mymodule.myfunc")',
261
+ )
262
+ diffbehavior_parser.add_argument(
263
+ "fn2",
264
+ metavar="FUNCTION2",
265
+ type=str,
266
+ help="second fully-qualified function to compare",
267
+ )
268
+ diffbehavior_parser.add_argument(
269
+ "--exception_equivalence",
270
+ metavar="EXCEPTION_EQUIVALENCE",
271
+ type=ExceptionEquivalenceType,
272
+ default=ExceptionEquivalenceType.TYPE_AND_MESSAGE,
273
+ choices=ExceptionEquivalenceType.__members__.values(),
274
+ help=textwrap.dedent(
275
+ """\
276
+ Decide how to treat exceptions, while searching for a counter-example.
277
+ `ALL` treats all exceptions as equivalent,
278
+ `SAME_TYPE`, considers matches on the type.
279
+ `TYPE_AND_MESSAGE` matches for the same type and message.
280
+ """
281
+ ),
282
+ )
283
+ cover_parser = subparsers.add_parser(
284
+ "cover",
285
+ formatter_class=argparse.RawTextHelpFormatter,
286
+ help="Generate inputs for a function, attempting to exercise different code paths",
287
+ description=textwrap.dedent(
288
+ """\
289
+ Generates inputs to a function, hopefully getting good line, branch,
290
+ and path coverage.
291
+ See https://crosshair.readthedocs.io/en/latest/cover.html
292
+ """
293
+ ),
294
+ parents=[common],
295
+ )
296
+ cover_parser.add_argument(
297
+ "target",
298
+ metavar="TARGET",
299
+ type=str,
300
+ nargs="+",
301
+ help=textwrap.dedent(
302
+ """\
303
+ A fully qualified module, class, or function, or
304
+ a directory (which will be recursively analyzed), or
305
+ a file path with an optional ":<line-number>" suffix.
306
+ See https://crosshair.readthedocs.io/en/latest/contracts.html#targeting
307
+ """
308
+ ),
309
+ )
310
+ cover_parser.add_argument(
311
+ "--example_output_format",
312
+ type=lambda e: ExampleOutputFormat[e.upper()], # type: ignore
313
+ choices=ExampleOutputFormat.__members__.values(),
314
+ metavar="FORMAT",
315
+ default=ExampleOutputFormat.EVAL_EXPRESSION,
316
+ help=textwrap.dedent(
317
+ """\
318
+ Determines how to output examples.
319
+ eval_expression : [default] Output examples as expressions,
320
+ suitable for eval()
321
+ arg_dictionary : Output arguments as repr'd, ordered
322
+ dictionaries
323
+ pytest : Output examples as stub pytest tests
324
+ argument_dictionary : Deprecated
325
+ """
326
+ ),
327
+ )
328
+ cover_parser.add_argument(
329
+ "--coverage_type",
330
+ type=lambda e: CoverageType[e.upper()], # type: ignore
331
+ choices=CoverageType.__members__.values(),
332
+ metavar="TYPE",
333
+ default=CoverageType.OPCODE,
334
+ help=textwrap.dedent(
335
+ """\
336
+ Determines what kind of coverage to achieve.
337
+ opcode : [default] Cover as many opcodes of the function as
338
+ possible. This is similar to "branch" coverage.
339
+ path : Cover any possible execution path.
340
+ There will usually be an infinite number of paths (e.g.
341
+ loops are effectively unrolled). Use
342
+ max_uninteresting_iterations and/or per_condition_timeout
343
+ to bound results.
344
+ Many path decisions are internal to CrossHair, so you may
345
+ see more duplicative-ness in the output than you'd expect.
346
+ """
347
+ ),
348
+ )
349
+ for subparser in (check_parser, search_parser, diffbehavior_parser, cover_parser):
350
+ subparser.add_argument(
351
+ "--max_uninteresting_iterations",
352
+ type=int,
353
+ help=textwrap.dedent(
354
+ """\
355
+ Maximum number of consecutive iterations to run without making
356
+ significant progress in exploring the codebase.
357
+ (by default, 5 iterations, unless --per_condition_timeout is set)
358
+
359
+ This option can be more useful than --per_condition_timeout
360
+ because the amount of time invested will scale with the complexity
361
+ of the code under analysis.
362
+
363
+ Use a small integer (3-5) for fast but weak analysis.
364
+ Values in the hundreds or thousands may be appropriate if you
365
+ intend to run CrossHair for hours.
366
+ """
367
+ ),
368
+ )
369
+
370
+ for subparser in (check_parser, search_parser, diffbehavior_parser, cover_parser):
371
+ subparser.add_argument(
372
+ "--per_path_timeout",
373
+ type=float,
374
+ metavar="FLOAT",
375
+ help=textwrap.dedent(
376
+ """\
377
+ Maximum seconds to spend checking one execution path.
378
+ If unspecified:
379
+ 1. CrossHair will timeout each path at the square root of
380
+ `--per_condition_timeout`, if specified.
381
+ 3. Otherwise, it will timeout each path at a number of seconds
382
+ equal to `--max_uninteresting_iterations`, unless it is
383
+ explicitly set to zero.
384
+ (NOTE: `--max_uninteresting_iterations` is 5 by default)
385
+ 2. Otherwise, it will not use any per-path timeout.
386
+ """
387
+ ),
388
+ )
389
+ subparser.add_argument(
390
+ "--per_condition_timeout",
391
+ type=float,
392
+ metavar="FLOAT",
393
+ help="Maximum seconds to spend checking execution paths for one condition",
394
+ )
395
+ lsp_server_parser = subparsers.add_parser(
396
+ "server",
397
+ help="Start a server, speaking the Language Server Protocol",
398
+ parents=[common],
399
+ formatter_class=argparse.RawTextHelpFormatter,
400
+ description=textwrap.dedent(
401
+ f"""\
402
+ Many IDEs support the Language Server Protocol (LSP).
403
+ CrossHair can produce various results and analysis through LSP.
404
+ """
405
+ ),
406
+ )
407
+
408
+ for subparser in (check_parser, watch_parser, lsp_server_parser):
409
+ subparser.add_argument(
410
+ "--analysis_kind",
411
+ type=analysis_kind,
412
+ metavar="KIND",
413
+ help=textwrap.dedent(
414
+ """\
415
+ Kind of contract to check.
416
+ By default, the PEP316, deal, and icontract kinds are all checked.
417
+ Multiple kinds (comma-separated) may be given.
418
+ See https://crosshair.readthedocs.io/en/latest/kinds_of_contracts.html
419
+ asserts : check assert statements
420
+ PEP316 : check PEP316 contracts (docstring-based)
421
+ icontract : check icontract contracts (decorator-based)
422
+ deal : check deal contracts (decorator-based)
423
+ """
424
+ ),
425
+ )
426
+ return parser
427
+
428
+
429
+ def run_watch_loop(
430
+ watcher: Watcher,
431
+ max_watch_iterations: int = sys.maxsize,
432
+ term_lines_rewritable: bool = True,
433
+ ) -> None:
434
+ restart = True
435
+ stats: Counter[str] = Counter()
436
+ active_messages: Dict[Tuple[str, int], AnalysisMessage]
437
+ for _ in range(max_watch_iterations):
438
+ if restart:
439
+ clear_screen()
440
+ print_divider("-")
441
+ line = f" Analyzing {len(watcher._modtimes)} files."
442
+ print(color(line, AnsiColor.OKBLUE), end="")
443
+ max_uninteresting_iterations = (
444
+ DEFAULT_OPTIONS.get_max_uninteresting_iterations()
445
+ )
446
+ restart = False
447
+ stats = Counter()
448
+ active_messages = {}
449
+ else:
450
+ time.sleep(0.1)
451
+ max_uninteresting_iterations = min(
452
+ max_uninteresting_iterations * 2, 100_000_000
453
+ )
454
+ for curstats, messages in watcher.run_iteration(max_uninteresting_iterations):
455
+ messages = [m for m in messages if m.state > MessageType.PRE_UNSAT]
456
+ stats.update(curstats)
457
+ if messages_merged(active_messages, messages):
458
+ linecache.checkcache()
459
+ clear_screen()
460
+ options = DEFAULT_OPTIONS.overlay(watcher._options)
461
+ for message in active_messages.values():
462
+ lines = long_describe_message(message, options)
463
+ if lines is None:
464
+ continue
465
+ print_divider("-")
466
+ print(lines, end="")
467
+ print_divider("-")
468
+ else:
469
+ if term_lines_rewritable:
470
+ print("\r", end="")
471
+ else:
472
+ print(".", end="")
473
+ continue
474
+ num_files = len(watcher._modtimes)
475
+ if len(watcher._paths) > 1:
476
+ loc_desc = f"{num_files} files"
477
+ else:
478
+ path_parts = Path(next(iter(watcher._paths))).parts
479
+ path_desc = path_parts[-1] if path_parts else "."
480
+ if num_files > 1:
481
+ loc_desc = f'"{path_desc}" ({num_files} files)'
482
+ else:
483
+ loc_desc = f'"{path_desc}"'
484
+ if term_lines_rewritable:
485
+ line = f' Analyzed {stats["num_paths"]} paths in {loc_desc}. '
486
+ else:
487
+ line = f" Analyzing paths in {loc_desc}: "
488
+ print(color(line, AnsiColor.OKBLUE), end="")
489
+ if watcher._change_flag:
490
+ watcher._change_flag = False
491
+ restart = True
492
+ line = f" Restarting analysis over {len(watcher._modtimes)} files."
493
+ print(color(line, AnsiColor.OKBLUE), end="")
494
+
495
+
496
+ def clear_screen():
497
+ # Print enough newlines to fill the screen:
498
+ print("\n" * shutil.get_terminal_size().lines, end="")
499
+
500
+
501
+ def print_divider(ch=" "):
502
+ try:
503
+ cols = os.get_terminal_size().columns - 1
504
+ except OSError:
505
+ cols = 5
506
+ print(ch * cols)
507
+
508
+
509
+ class AnsiColor(enum.Enum):
510
+ HEADER = "\033[95m"
511
+ OKBLUE = "\033[94m"
512
+ OKGREEN = "\033[92m"
513
+ WARNING = "\033[93m"
514
+ FAIL = "\033[91m"
515
+ ENDC = "\033[0m"
516
+ BOLD = "\033[1m"
517
+ UNDERLINE = "\033[4m"
518
+
519
+
520
+ def color(text: str, *effects: AnsiColor) -> str:
521
+ return "".join(e.value for e in effects) + text + AnsiColor.ENDC.value
522
+
523
+
524
+ def messages_merged(
525
+ messages: MutableMapping[Tuple[str, int], AnalysisMessage],
526
+ new_messages: Iterable[AnalysisMessage],
527
+ ) -> bool:
528
+ any_change = False
529
+ for message in new_messages:
530
+ key = (message.filename, message.line)
531
+ if key not in messages:
532
+ messages[key] = message
533
+ any_change = True
534
+ return any_change
535
+
536
+
537
+ _MOTD = [
538
+ "Did I miss a counterexample? Let me know: https://github.com/pschanely/CrossHair/issues/new",
539
+ "Help me be faster! Add to my benchmark suite: https://github.com/pschanely/crosshair-benchmark",
540
+ "Please consider sharing your CrossHair experience with others on social media.",
541
+ "Questions? Ask at https://github.com/pschanely/CrossHair/discussions/new?category=q-a",
542
+ "Consider signing up for CrossHair updates at https://pschanely.github.io",
543
+ "Come say hello in the discord chat; we are friendly! https://discord.gg/rUeTaYTWbb",
544
+ ]
545
+
546
+
547
+ def watch(
548
+ args: argparse.Namespace,
549
+ options: AnalysisOptionSet,
550
+ max_watch_iterations=sys.maxsize,
551
+ ) -> int:
552
+ if not args.directory:
553
+ print("No files or directories given to watch", file=sys.stderr)
554
+ return 2
555
+ try:
556
+ paths = [Path(d) for d in args.directory]
557
+
558
+ # While the watcher is tolerant of files and directories disappearing mid-run,
559
+ # we still expect them to exist at launch time to make typos obvious:
560
+ nonexistent = [p for p in paths if not p.exists()]
561
+ if nonexistent:
562
+ print(
563
+ f"File(s) not found: {', '.join(map(str, nonexistent))}",
564
+ file=sys.stderr,
565
+ )
566
+ return 2
567
+
568
+ watcher = Watcher(paths, options)
569
+ watcher.check_changed()
570
+
571
+ # Some terminals don't interpret \r correctly; we detect them here:
572
+ term_lines_rewritable = "THONNY_USER_DIR" not in os.environ
573
+
574
+ run_watch_loop(
575
+ watcher, max_watch_iterations, term_lines_rewritable=term_lines_rewritable
576
+ )
577
+ except KeyboardInterrupt:
578
+ pass
579
+ watcher._pool.terminate()
580
+ print()
581
+ if random.uniform(0.0, 1.0) > 0.4:
582
+ motd = "I enjoyed working with you today!"
583
+ else:
584
+ motd = random.choice(_MOTD)
585
+ print(motd)
586
+ return 0
587
+
588
+
589
+ def format_src_context(filename: str, lineno: int) -> str:
590
+ amount = 3
591
+ line_numbers = range(max(1, lineno - amount), lineno + amount + 1)
592
+ output = [f"{filename}:{lineno}:\n"]
593
+ for curline in line_numbers:
594
+ text = linecache.getline(filename, curline)
595
+ if text == "": # (actual empty lines have a newline)
596
+ continue
597
+ output.append(
598
+ ">" + color(text, AnsiColor.WARNING) if lineno == curline else "|" + text
599
+ )
600
+ return "".join(output)
601
+
602
+
603
+ def describe_message(
604
+ message: AnalysisMessage, options: AnalysisOptions
605
+ ) -> Optional[str]:
606
+ if options.report_verbose:
607
+ return long_describe_message(message, options)
608
+ else:
609
+ return short_describe_message(message, options)
610
+
611
+
612
+ def long_describe_message(
613
+ message: AnalysisMessage, options: AnalysisOptions
614
+ ) -> Optional[str]:
615
+ tb, desc, state = message.traceback, message.message, message.state
616
+ desc = desc.replace(" when ", "\nwhen ")
617
+ context = format_src_context(message.filename, message.line)
618
+ intro = ""
619
+ if not options.report_all:
620
+ if message.state <= MessageType.PRE_UNSAT: # type: ignore
621
+ return None
622
+ if state == MessageType.CONFIRMED:
623
+ intro = "I was able to confirm your postcondition over all paths."
624
+ elif state == MessageType.CANNOT_CONFIRM:
625
+ intro = "I wasn't able to find a counterexample."
626
+ elif message.state == MessageType.PRE_UNSAT:
627
+ intro = "I am having trouble finding any inputs that meet your preconditions."
628
+ elif message.state == MessageType.POST_ERR:
629
+ intro = "I got an error while checking your postcondition."
630
+ elif message.state == MessageType.EXEC_ERR:
631
+ intro = "I found an exception while running your function."
632
+ elif message.state == MessageType.POST_FAIL:
633
+ intro = "I was able to make your postcondition return False."
634
+ elif message.state == MessageType.SYNTAX_ERR:
635
+ intro = "One of your conditions isn't a valid python expression."
636
+ elif message.state == MessageType.IMPORT_ERR:
637
+ intro = "I couldn't import a file."
638
+ if message.state <= MessageType.CANNOT_CONFIRM: # type: ignore
639
+ intro = color(intro, AnsiColor.OKGREEN)
640
+ else:
641
+ intro = color(intro, AnsiColor.FAIL)
642
+ return f"{tb}\n{intro}\n{context}\n{desc}\n"
643
+
644
+
645
+ def short_describe_message(
646
+ message: AnalysisMessage, options: AnalysisOptions
647
+ ) -> Optional[str]:
648
+ desc = message.message
649
+ if message.state <= MessageType.PRE_UNSAT: # type: ignore
650
+ if options.report_all:
651
+ return "{}:{}: {}: {}".format(message.filename, message.line, "info", desc)
652
+ return None
653
+ if message.state == MessageType.POST_ERR:
654
+ desc = "Error while evaluating post condition: " + desc
655
+ return "{}:{}: {}: {}".format(message.filename, message.line, "error", desc)
656
+
657
+
658
+ def checked_fn_load(qualname: str, stderr: TextIO) -> Optional[FunctionInfo]:
659
+ try:
660
+ objs = list(load_files_or_qualnames([qualname]))
661
+ except ErrorDuringImport as exc:
662
+ cause = exc.__cause__ if exc.__cause__ is not None else exc
663
+ print(
664
+ f'Unable to load "{qualname}": {type(cause).__name__}: {cause}',
665
+ file=stderr,
666
+ )
667
+ return None
668
+ obj = objs[0]
669
+ if not isinstance(obj, FunctionInfo):
670
+ print(f'"{qualname}" does not target a function.', file=stderr)
671
+ return None
672
+ if obj.get_callable() is None:
673
+ print(f'Cannot determine signature of "{qualname}"', file=stderr)
674
+ return None
675
+ return obj
676
+
677
+
678
+ def checked_load(
679
+ target: str, stderr: TextIO
680
+ ) -> Union[int, Iterable[Union[ModuleType, type, FunctionInfo]]]:
681
+ try:
682
+ return list(load_files_or_qualnames(target))
683
+ except FileNotFoundError as exc:
684
+ print(f'File not found: "{exc.args[0]}"', file=stderr)
685
+ return 2
686
+ except ErrorDuringImport as exc:
687
+ cause = exc.__cause__ if exc.__cause__ is not None else exc
688
+ print(f"Could not import your code:\n", file=stderr)
689
+ traceback.print_exception(type(cause), cause, cause.__traceback__, file=stderr)
690
+ return 2
691
+
692
+
693
+ def diffbehavior(
694
+ args: argparse.Namespace, options: AnalysisOptions, stdout: TextIO, stderr: TextIO
695
+ ) -> int:
696
+ (fn_name1, fn_name2) = (args.fn1, args.fn2)
697
+ fn1 = checked_fn_load(fn_name1, stderr)
698
+ fn2 = checked_fn_load(fn_name2, stderr)
699
+ exception_equivalence = args.exception_equivalence
700
+ if fn1 is None or fn2 is None:
701
+ return 2
702
+ options.stats = Counter()
703
+ diffs = diff_behavior(fn1, fn2, options, exception_equivalence)
704
+ debug("stats", options.stats)
705
+ if isinstance(diffs, str):
706
+ print(diffs, file=stderr)
707
+ return 2
708
+ elif len(diffs) == 0:
709
+ num_paths = options.stats["num_paths"]
710
+ exhausted = options.stats["exhaustion"] > 0
711
+ stdout.write(f"No differences found. (attempted {num_paths} iterations)\n")
712
+ if exhausted:
713
+ stdout.write("All paths exhausted, functions are likely the same!\n")
714
+ else:
715
+ stdout.write(
716
+ "Consider increasing the --max_uninteresting_iterations option.\n"
717
+ )
718
+ return 0
719
+ else:
720
+ width = max(len(fn_name1), len(fn_name2)) + 2
721
+ for diff in diffs:
722
+ inputs = ", ".join(f"{k}={v}" for k, v in diff.args.items())
723
+ stdout.write(f"Given: ({inputs}),\n")
724
+ result1, result2 = diff.result1, diff.result2
725
+ differing_args = result1.get_differing_arg_mutations(result2)
726
+ stdout.write(
727
+ f"{fn_name1.rjust(width)} : {result1.describe(differing_args)}\n"
728
+ )
729
+ stdout.write(
730
+ f"{fn_name2.rjust(width)} : {result2.describe(differing_args)}\n"
731
+ )
732
+ return 1
733
+
734
+
735
+ def cover(
736
+ args: argparse.Namespace, options: AnalysisOptions, stdout: TextIO, stderr: TextIO
737
+ ) -> int:
738
+ entities = checked_load(args.target, stderr)
739
+ if isinstance(entities, int):
740
+ return entities
741
+ to_be_processed = deque(entities)
742
+ fns = []
743
+ while to_be_processed:
744
+ entity = to_be_processed.pop()
745
+ if isinstance(entity, ModuleType):
746
+ to_be_processed.extend(
747
+ v for k, v in get_top_level_classes_and_functions(entity)
748
+ )
749
+ elif isinstance(entity, FunctionInfo):
750
+ fns.append(entity)
751
+ else:
752
+ assert isinstance(entity, type)
753
+ fns.extend(
754
+ FunctionInfo.from_class(entity, n)
755
+ for n, e in entity.__dict__.items()
756
+ if isinstance(e, FUNCTIONINFO_DESCRIPTOR_TYPES)
757
+ )
758
+
759
+ if not fns:
760
+ print("No functions or methods found.", file=stderr)
761
+ return 2
762
+ example_output_format = args.example_output_format
763
+ options.stats = Counter()
764
+ imports, lines = set(), []
765
+ for ctxfn in fns:
766
+ debug("Begin cover on", ctxfn.name)
767
+ pair = ctxfn.get_callable()
768
+ if pair is None:
769
+ continue
770
+ fn = pair[0]
771
+
772
+ try:
773
+ paths = path_cover(
774
+ ctxfn,
775
+ options,
776
+ args.coverage_type,
777
+ arg_formatter=(
778
+ format_boundargs_as_dictionary
779
+ if example_output_format == ExampleOutputFormat.ARG_DICTIONARY
780
+ else format_boundargs
781
+ ),
782
+ )
783
+ except NotDeterministic:
784
+ print(
785
+ "Repeated executions are not behaving deterministically.", file=stderr
786
+ )
787
+ if not in_debug():
788
+ print("Re-run in verbose mode for debugging information.", file=stderr)
789
+ return 2
790
+ if example_output_format == ExampleOutputFormat.ARG_DICTIONARY:
791
+ output_argument_dictionary_paths(fn, paths, stdout, stderr)
792
+ elif example_output_format == ExampleOutputFormat.EVAL_EXPRESSION:
793
+ output_eval_exression_paths(fn, paths, stdout, stderr)
794
+ elif example_output_format == ExampleOutputFormat.PYTEST:
795
+ (cur_imports, cur_lines) = output_pytest_paths(fn, paths)
796
+ imports |= cur_imports
797
+ # imports.add(f"import {fn.__qualname__}")
798
+ lines.extend(cur_lines)
799
+ else:
800
+ assert False, "unexpected output format"
801
+ if example_output_format == ExampleOutputFormat.PYTEST:
802
+ stdout.write("\n".join(sorted(imports) + [""] + lines) + "\n")
803
+ stdout.flush()
804
+
805
+ return 0
806
+
807
+
808
+ def search(
809
+ args: argparse.Namespace, options: AnalysisOptions, stdout: TextIO, stderr: TextIO
810
+ ):
811
+ ctxfn = checked_fn_load(args.fn, stderr)
812
+ if ctxfn is None:
813
+ return 2
814
+ fn, _ = ctxfn.callable()
815
+
816
+ score: Optional[Callable] = None
817
+ optimization_kind: OptimizationKind = args.optimization
818
+ output_all_examples: bool = args.output_all_examples
819
+
820
+ argument_formatter = args.argument_formatter
821
+ if argument_formatter:
822
+ argument_formatter = checked_fn_load(argument_formatter, stderr)
823
+ if argument_formatter is None:
824
+ return 2
825
+ else:
826
+ argument_formatter, _ = argument_formatter.callable()
827
+
828
+ final_example: Optional[str] = None
829
+
830
+ def on_example(example: str) -> None:
831
+ if output_all_examples:
832
+ stdout.write(example + "\n")
833
+ nonlocal final_example
834
+ final_example = example
835
+
836
+ path_search(
837
+ ctxfn, options, argument_formatter, optimization_kind, score, on_example
838
+ )
839
+ if final_example is None:
840
+ stderr.write("No input found.\n")
841
+ stderr.write("Consider increasing the --max_uninteresting_iterations option.\n")
842
+ return 1
843
+ else:
844
+ if not output_all_examples:
845
+ stdout.write(final_example + "\n")
846
+ return 0
847
+
848
+
849
+ def server(
850
+ args: argparse.Namespace, options: AnalysisOptionSet, stdout: TextIO, stderr: TextIO
851
+ ) -> NoReturn:
852
+ from crosshair.lsp_server import create_lsp_server # (defer import for performance)
853
+
854
+ cast(Callable[[], NoReturn], create_lsp_server(options).start_io)()
855
+
856
+
857
+ def check(
858
+ args: argparse.Namespace, options: AnalysisOptionSet, stdout: TextIO, stderr: TextIO
859
+ ) -> int:
860
+ any_problems = False
861
+ entities = checked_load(args.target, stderr)
862
+ if isinstance(entities, int):
863
+ return entities
864
+ full_options = DEFAULT_OPTIONS.overlay(report_verbose=False).overlay(options)
865
+ checkables = [c for e in entities for c in analyze_any(e, options)]
866
+ if not checkables:
867
+ extra_help = ""
868
+ if full_options.analysis_kind == [AnalysisKind.asserts]:
869
+ extra_help = "\nHINT: Ensure that your functions to analyze lead with assert statements."
870
+ print(
871
+ "WARNING: Targets found, but contain no checkable functions." + extra_help,
872
+ file=stderr,
873
+ )
874
+
875
+ for message in run_checkables(checkables):
876
+ line = describe_message(message, full_options)
877
+ if line is None:
878
+ continue
879
+ stdout.write(line + "\n")
880
+ debug("Traceback for output message:\n", message.traceback)
881
+ if message.state > MessageType.PRE_UNSAT:
882
+ any_problems = True
883
+ return 1 if any_problems else 0
884
+
885
+
886
+ def unwalled_main(cmd_args: Union[List[str], argparse.Namespace]) -> int:
887
+ parser = command_line_parser()
888
+ if isinstance(cmd_args, argparse.Namespace):
889
+ args = cmd_args
890
+ else:
891
+ args = parser.parse_args(cmd_args)
892
+ if not args.action:
893
+ parser.print_help(sys.stderr)
894
+ return 2
895
+ set_debug(args.verbose)
896
+ if in_debug():
897
+ debug(env_info())
898
+ debug("Installed plugins:", installed_plugins)
899
+ options = option_set_from_dict(args.__dict__)
900
+ # fall back to current directory to look up modules
901
+ path_additions = [""] if sys.path and sys.path[0] != "" else []
902
+ with add_to_pypath(*path_additions), prefer_pure_python_imports():
903
+ if args.extra_plugin:
904
+ for plugin in args.extra_plugin:
905
+ exec(Path(plugin).read_text())
906
+ if len(REGISTERED_CONTRACTS):
907
+ debug(
908
+ f"Registered {len(REGISTERED_CONTRACTS)} contract(s) "
909
+ f"from: {args.extra_plugin}"
910
+ )
911
+ if args.action == "check":
912
+ return check(args, options, sys.stdout, sys.stderr)
913
+ elif args.action == "search":
914
+ return search(
915
+ args, DEFAULT_OPTIONS.overlay(options), sys.stdout, sys.stderr
916
+ )
917
+ elif args.action == "diffbehavior":
918
+ defaults = DEFAULT_OPTIONS.overlay(
919
+ AnalysisOptionSet(
920
+ per_path_timeout=30.0, # mostly, we don't want to time out paths
921
+ )
922
+ )
923
+ return diffbehavior(args, defaults.overlay(options), sys.stdout, sys.stderr)
924
+ elif args.action == "cover":
925
+ defaults = DEFAULT_OPTIONS.overlay(
926
+ AnalysisOptionSet(
927
+ per_path_timeout=30.0, # mostly, we don't want to time out paths
928
+ )
929
+ )
930
+ return cover(args, defaults.overlay(options), sys.stdout, sys.stderr)
931
+ elif args.action == "watch":
932
+ disable_auditwall() # (we'll engage auditwall in the workers)
933
+ return watch(args, options)
934
+ elif args.action == "server":
935
+ disable_auditwall() # (we'll engage auditwall in the workers)
936
+ server(args, options, sys.stdout, sys.stderr)
937
+ else:
938
+ print(f'Unknown action: "{args.action}"', file=sys.stderr)
939
+ return 2
940
+
941
+
942
+ def mypy_and_check(cmd_args: Optional[List[str]] = None) -> None:
943
+ if cmd_args is None:
944
+ cmd_args = sys.argv[1:]
945
+ cmd_args = ["check"] + cmd_args
946
+ check_args, mypy_args = command_line_parser().parse_known_args(cmd_args)
947
+ set_debug(check_args.verbose)
948
+ mypy_cmd_args = mypy_args + check_args.target
949
+ debug("Running mypy with the following arguments:", " ".join(mypy_cmd_args))
950
+ try:
951
+ from mypy import api
952
+ except ModuleNotFoundError:
953
+ print("Unable to find mypy; skipping", file=sys.stderr)
954
+ else:
955
+ _mypy_out, mypy_err, mypy_ret = api.run(mypy_cmd_args)
956
+ print(mypy_err, file=sys.stderr)
957
+ if mypy_ret != 0:
958
+ print(_mypy_out, file=sys.stdout)
959
+ sys.exit(mypy_ret)
960
+ engage_auditwall()
961
+ debug("Running crosshair with these args:", check_args)
962
+ sys.exit(unwalled_main(check_args))
963
+
964
+
965
+ def main(cmd_args: Optional[List[str]] = None) -> None:
966
+ if cmd_args is None:
967
+ cmd_args = sys.argv[1:]
968
+ engage_auditwall()
969
+ sys.exit(unwalled_main(cmd_args))
970
+
971
+
972
+ if __name__ == "__main__":
973
+ main()