dv-solve 0.2.3__py3-none-win_amd64.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 (78) hide show
  1. dv_solve/__init__.py +253 -0
  2. dv_solve/__version__.py +42 -0
  3. dv_solve/_resolve.py +594 -0
  4. dv_solve/buffer_inference.py +181 -0
  5. dv_solve/builder.py +451 -0
  6. dv_solve/bvsat.py +235 -0
  7. dv_solve/ctx.py +483 -0
  8. dv_solve/dv_solve.dll +0 -0
  9. dv_solve/dv_solve.lib +0 -0
  10. dv_solve/dv_solve_debug.dll +0 -0
  11. dv_solve/dv_solve_debug.lib +0 -0
  12. dv_solve/dv_solve_dpi.dll +0 -0
  13. dv_solve/dv_solve_dpi.lib +0 -0
  14. dv_solve/flow_constraint_store.py +114 -0
  15. dv_solve/icl.py +96 -0
  16. dv_solve/lib.py +385 -0
  17. dv_solve/partitioner.py +119 -0
  18. dv_solve/problem.py +282 -0
  19. dv_solve/scheduling_graph.py +293 -0
  20. dv_solve/share/include/dv_solve/compat/win/msvc_compat.h +130 -0
  21. dv_solve/share/include/dv_solve/compat/win/sys/resource.h +26 -0
  22. dv_solve/share/include/dv_solve/compat/win/sys/time.h +25 -0
  23. dv_solve/share/include/dv_solve/compat/win/unistd.h +42 -0
  24. dv_solve/share/include/dv_solve/dv_solve.h +407 -0
  25. dv_solve/share/include/dv_solve/dvs_aig.h +107 -0
  26. dv_solve/share/include/dv_solve/dvs_aig_cnf.h +65 -0
  27. dv_solve/share/include/dv_solve/dvs_alloc.h +30 -0
  28. dv_solve/share/include/dv_solve/dvs_arena.h +110 -0
  29. dv_solve/share/include/dv_solve/dvs_bbsolver.h +289 -0
  30. dv_solve/share/include/dv_solve/dvs_bitblast.h +102 -0
  31. dv_solve/share/include/dv_solve/dvs_block_alloc.h +69 -0
  32. dv_solve/share/include/dv_solve/dvs_builder.h +199 -0
  33. dv_solve/share/include/dv_solve/dvs_bvbounds.h +116 -0
  34. dv_solve/share/include/dv_solve/dvs_bvdom.h +138 -0
  35. dv_solve/share/include/dv_solve/dvs_contradiction.h +173 -0
  36. dv_solve/share/include/dv_solve/dvs_costguided.h +238 -0
  37. dv_solve/share/include/dv_solve/dvs_ctx.h +411 -0
  38. dv_solve/share/include/dv_solve/dvs_cube.h +44 -0
  39. dv_solve/share/include/dv_solve/dvs_diffcycle.h +54 -0
  40. dv_solve/share/include/dv_solve/dvs_dpi.h +139 -0
  41. dv_solve/share/include/dv_solve/dvs_explain.h +134 -0
  42. dv_solve/share/include/dv_solve/dvs_i128.h +157 -0
  43. dv_solve/share/include/dv_solve/dvs_lcg.h +304 -0
  44. dv_solve/share/include/dv_solve/dvs_placement.h +157 -0
  45. dv_solve/share/include/dv_solve/dvs_pool.h +83 -0
  46. dv_solve/share/include/dv_solve/dvs_problem.h +514 -0
  47. dv_solve/share/include/dv_solve/dvs_propagator.h +561 -0
  48. dv_solve/share/include/dv_solve/dvs_sat.h +226 -0
  49. dv_solve/share/include/dv_solve/dvs_sat_backend.h +87 -0
  50. dv_solve/share/include/dv_solve/dvs_search.h +144 -0
  51. dv_solve/share/include/dv_solve/dvs_shave.h +31 -0
  52. dv_solve/share/include/dv_solve/dvs_stack.h +79 -0
  53. dv_solve/share/include/dv_solve/dvs_stackinfo.h +39 -0
  54. dv_solve/share/include/dv_solve/dvs_sv.h +105 -0
  55. dv_solve/share/include/dv_solve/dvs_thread.h +86 -0
  56. dv_solve/share/include/dv_solve/dvs_trail.h +146 -0
  57. dv_solve/share/include/dv_solve/dvs_variable.h +79 -0
  58. dv_solve/share/include/dv_solve/dvs_wiremask.h +81 -0
  59. dv_solve/share/include/dv_solve/smt2/smt2_frontend.h +620 -0
  60. dv_solve/share/include/dv_solve/smt2/smt2_lexer.h +106 -0
  61. dv_solve/share/include/dv_solve/smt2/smt2_parser.h +90 -0
  62. dv_solve/share/include/dv_solve/svdpi_compat.h +60 -0
  63. dv_solve/share/sv/dvs_dpi_pkg.sv +74 -0
  64. dv_solve/share/sv/dvs_randomizer_pkg.sv +76 -0
  65. dv_solve/state_graph.py +249 -0
  66. dv_solve/stream_solver.py +304 -0
  67. dv_solve/structural_solver.py +297 -0
  68. dv_solve-0.2.3.data/scripts/dv-solve-smt2.exe +0 -0
  69. dv_solve-0.2.3.dist-info/DELVEWHEEL +2 -0
  70. dv_solve-0.2.3.dist-info/METADATA +110 -0
  71. dv_solve-0.2.3.dist-info/RECORD +78 -0
  72. dv_solve-0.2.3.dist-info/WHEEL +5 -0
  73. dv_solve-0.2.3.dist-info/licenses/LICENSE +201 -0
  74. dv_solve-0.2.3.dist-info/top_level.txt +1 -0
  75. dv_solve.libs/.load-order-dv_solve-0.2.3 +3 -0
  76. dv_solve.libs/msvcp140.dll +0 -0
  77. dv_solve.libs/vcruntime140.dll +0 -0
  78. dv_solve.libs/vcruntime140_1.dll +0 -0
dv_solve/_resolve.py ADDED
@@ -0,0 +1,594 @@
1
+ """The single source of truth for locating dv-solve's native artifacts.
2
+
3
+ Both consumers of this module must agree, or a build silently mixes two
4
+ installations:
5
+
6
+ * ``lib.py`` dlopen()s the core library for the Python API;
7
+ * ``__init__.py``'s ``get_libdirs`` / ``get_dpi_lib`` / ``get_incdirs`` /
8
+ ``get_svdirs`` tell a C consumer what to compile and link against.
9
+
10
+ Those two used to search different places in a different order -- the loader
11
+ consulted ``LD_LIBRARY_PATH``, the package's ``lib/`` subdirectory, three
12
+ alternate build-directory names and a ``/tmp`` pytest glob that the public
13
+ helpers knew nothing about. A host with any of those set could therefore run
14
+ the solver out of one installation while linking generated C against another,
15
+ and the ABI mismatch surfaces as a crash inside ``dvs_solver_compile``, nowhere
16
+ near its cause. Hence one resolver, used by both.
17
+
18
+ THE CONTRACT
19
+ ------------
20
+
21
+ The unit of discovery is an INSTALLATION, not an artifact. An installation is
22
+ one root together with the places its libraries, headers and SV sources live.
23
+ Candidates, best first:
24
+
25
+ 1. ``DVS_SOLVER_PATH`` (or the legacy ``ZSP_SOLVER_PATH``) -- the explicit
26
+ override. When set it is the ONLY
27
+ candidate: it is selected whether or not it is usable, and anything it
28
+ lacks is reported as missing from it rather than supplied by a later
29
+ candidate. Accepts a directory holding the artifacts or an install
30
+ prefix with the usual ``lib/``, ``lib64/``, ``include/``, ``share/``
31
+ subdirectories.
32
+ 2. The installed package: the directory holding ``__init__.py``, then its
33
+ ``lib/`` subdirectory. This is the binary-wheel layout.
34
+ 3. A built source checkout, one candidate per supported build-directory
35
+ name: ``<root>/<build>`` and its ``lib``/``lib64`` install destinations.
36
+ 4. Each ``LD_LIBRARY_PATH`` entry -- a last-resort, library-only fallback,
37
+ NOT an override. It supplies no headers or SV sources.
38
+
39
+ SELECTION: the override if set, otherwise the first candidate holding a
40
+ loadable core library (``libdv_solve.so`` or a numerically versioned
41
+ ``libdv_solve.so.N...``). Every artifact -- core library for the loader, core
42
+ library for the linker, DPI library, headers, SV sources -- is then taken
43
+ from the selected installation and from nowhere else. An artifact the selected
44
+ installation lacks is MISSING; it is never borrowed from another candidate,
45
+ because that is exactly how a build mixes two solver builds.
46
+
47
+ LOADING VS LINKING within the selected installation: the unversioned name is
48
+ preferred for both, so whenever the installation is linkable the loader opens
49
+ the very file ``-ldv_solve`` resolves. An installation holding only a
50
+ versioned library is loadable but NOT linkable; ``require_library(...,
51
+ linkable=True)`` then raises naming that installation, rather than letting the
52
+ linker fall through to a different one.
53
+
54
+ When no candidate holds a core library at all, nothing is selected. Headers
55
+ and SV sources may still be located (from the first candidate that has them)
56
+ for compile-only consumers; there is no library for them to disagree with.
57
+
58
+ Rationale for (4) being last. It used to come second, ahead of the package,
59
+ which meant an ambient setting -- a simulator's wrapper script, a module-load,
60
+ a CI runner's default -- silently redirected the loader while the public
61
+ helpers, which never consulted it at all, kept reporting the package. That is
62
+ precisely the loader/linker split this module exists to prevent. ``LD_LIBRARY_
63
+ PATH`` still resolves the *dependencies* of whatever library is chosen; it
64
+ chooses only when nothing else can. Callers who genuinely want to select an
65
+ out-of-tree build have ``DVS_SOLVER_PATH``, which is unambiguous about intent.
66
+
67
+ Everything here probes for the ARTIFACT, never for the directory, and an
68
+ artifact must be a real file: a directory named ``libdv_solve.so`` or a
69
+ dangling symlink is not a library (a valid symlink to a library is). A
70
+ configured but unbuilt CMake tree has a ``build/lib`` the moment it is
71
+ configured, and an unbuilt source checkout has a ``src/c`` full of headers
72
+ but no library; a directory existing says nothing about whether the thing you
73
+ need is in it.
74
+ """
75
+ from __future__ import annotations
76
+
77
+ import os
78
+ import platform
79
+ import re
80
+ from typing import List, NamedTuple, Optional
81
+
82
+ #: Build-directory names a checkout may use. ``build`` is what this project's
83
+ #: own instructions and CI produce; the rest are long-supported aliases that
84
+ #: the ctypes loader has always accepted, kept so that dropping them is not a
85
+ #: silent regression for anyone whose tree uses one.
86
+ _BUILD_DIR_NAMES = ("build", "_build", "build_release", "cmake-build-release")
87
+
88
+ #: Where CMake puts libraries under a build directory. The empty string is the
89
+ #: pre-install link output that sits in the build root itself, which makes a
90
+ #: checkout that has been built but never ``cmake --install``-ed resolve.
91
+ _LIB_SUBDIRS = ("lib", "lib64", "")
92
+
93
+ #: A header that exists in every dv-solve include tree, used to tell a real
94
+ #: include directory from a directory that merely exists.
95
+ _SENTINEL_HEADER = "dv_solve.h"
96
+
97
+ #: Likewise for the SystemVerilog resource directory.
98
+ _SENTINEL_SV = "dvs_dpi_pkg.sv"
99
+
100
+
101
+ def _pkg_dir() -> str:
102
+ return os.path.dirname(os.path.abspath(__file__))
103
+
104
+
105
+ def _src_root() -> str:
106
+ """``packages/dv-solve/`` when imported from a source tree (or editable
107
+ install). Meaningless for a wheel installed into site-packages, where it
108
+ points at some interpreter-internal directory -- harmless, because every
109
+ path built from it is then probed for an artifact that will not be there.
110
+ """
111
+ return os.path.abspath(os.path.join(_pkg_dir(), "..", ".."))
112
+
113
+
114
+ def lib_filename(stem: str) -> str:
115
+ """The platform's file name for shared library *stem* (no directory)."""
116
+ system = platform.system()
117
+ if system == "Windows":
118
+ return "%s.dll" % stem
119
+ if system == "Darwin":
120
+ return "lib%s.dylib" % stem
121
+ return "lib%s.so" % stem
122
+
123
+
124
+ def lib_patterns(stem: str) -> List[str]:
125
+ """Glob patterns for library *stem*, most-preferred first.
126
+
127
+ The unversioned name comes first because it is the only one a linker will
128
+ accept for ``-l<stem>``; the soname-suffixed form is loadable by dlopen but
129
+ not linkable, so preferring it would let the loader and the linker pick
130
+ different files.
131
+ """
132
+ system = platform.system()
133
+ if system == "Windows":
134
+ return ["%s.dll" % stem, "lib%s.dll" % stem]
135
+ if system == "Darwin":
136
+ return ["lib%s.dylib" % stem, "lib%s.*.dylib" % stem]
137
+ return ["lib%s.so" % stem, "lib%s.so.*" % stem]
138
+
139
+
140
+ class Installation(NamedTuple):
141
+ """One candidate root and where its artifacts may live.
142
+
143
+ ``inc_sets`` is a list of include-directory SETS rather than directories:
144
+ the installed layout needs two entries (see :func:`find_incdirs`).
145
+ """
146
+ kind: str # "override" | "package" | "checkout" | "ld_library_path"
147
+ root: str
148
+ lib_dirs: List[str]
149
+ inc_sets: List[List[str]]
150
+ sv_dirs: List[str]
151
+
152
+ def describe(self) -> str:
153
+ if self.kind == "override":
154
+ return "%s=%s" % (_override_var(), self.root)
155
+ if self.kind == "ld_library_path":
156
+ var = "PATH" if platform.system() == "Windows" else "LD_LIBRARY_PATH"
157
+ return "%s entry %s" % (var, self.root)
158
+ return "%s installation at %s" % (self.kind, self.root)
159
+
160
+
161
+ def override_root() -> Optional[str]:
162
+ """The ``DVS_SOLVER_PATH`` value, or the legacy ``ZSP_SOLVER_PATH`` when
163
+ only that is set (zuspec-be-sw still sets the old name). ``None`` when
164
+ neither is set."""
165
+ return (os.environ.get("DVS_SOLVER_PATH")
166
+ or os.environ.get("ZSP_SOLVER_PATH") or None)
167
+
168
+
169
+ def _override_var() -> str:
170
+ """The name of the override variable in effect, for messages."""
171
+ if os.environ.get("DVS_SOLVER_PATH") or not os.environ.get("ZSP_SOLVER_PATH"):
172
+ return "DVS_SOLVER_PATH"
173
+ return "ZSP_SOLVER_PATH"
174
+
175
+
176
+ def _override() -> Optional[Installation]:
177
+ """The ``DVS_SOLVER_PATH`` installation, or ``None`` when unset.
178
+
179
+ The variable has historically pointed straight at a directory containing
180
+ the library, so that stays first. A prefix-style value (one with ``lib/``,
181
+ ``include/`` or ``share/`` underneath) also works, because a user who
182
+ points at a CMake install prefix reasonably expects that to be understood.
183
+ """
184
+ root = override_root()
185
+ if not root:
186
+ return None
187
+ j = os.path.join
188
+ inc_sets = [[root]]
189
+ for base in (j(root, "include"), j(root, "share", "include")):
190
+ inc_sets.append([base, j(base, "dv_solve")])
191
+ return Installation(
192
+ "override", root,
193
+ lib_dirs=[root, j(root, "lib"), j(root, "lib64")],
194
+ inc_sets=inc_sets,
195
+ sv_dirs=[root, j(root, "share", "sv"), j(root, "sv")])
196
+
197
+
198
+ def _package() -> Installation:
199
+ pkg, j = _pkg_dir(), os.path.join
200
+ return Installation(
201
+ "package", pkg,
202
+ lib_dirs=[pkg, j(pkg, "lib")],
203
+ inc_sets=[[b, j(b, "dv_solve")]
204
+ for b in (j(pkg, "share", "include"), j(pkg, "include"))],
205
+ sv_dirs=[j(pkg, "share", "sv")])
206
+
207
+
208
+ def _checkouts() -> List[Installation]:
209
+ """One candidate per build directory of a source checkout.
210
+
211
+ The checkout's flat ``src/c`` headers and ``src/sv`` sources belong to
212
+ every build of it; a build's own ``include``/``share/sv`` install tree
213
+ follows as an alternative.
214
+ """
215
+ root, j = _src_root(), os.path.join
216
+ out = []
217
+ for name in _BUILD_DIR_NAMES:
218
+ bd = j(root, name)
219
+ out.append(Installation(
220
+ "checkout", bd,
221
+ lib_dirs=[j(bd, sub) if sub else bd for sub in _LIB_SUBDIRS],
222
+ inc_sets=[[j(root, "src", "c")],
223
+ [j(bd, "include"), j(bd, "include", "dv_solve")]],
224
+ sv_dirs=[j(root, "src", "sv"), j(bd, "share", "sv")]))
225
+ return out
226
+
227
+
228
+ def _ld_library_path_dirs() -> List[str]:
229
+ var = "PATH" if platform.system() == "Windows" else "LD_LIBRARY_PATH"
230
+ return [d for d in os.environ.get(var, "").split(os.pathsep) if d]
231
+
232
+
233
+ def candidates() -> List[Installation]:
234
+ """Every candidate installation, best first. See the module docstring."""
235
+ override = _override()
236
+ if override is not None:
237
+ return [override] # terminal: nothing else competes
238
+ out = [_package()] + _checkouts()
239
+ out += [Installation("ld_library_path", d, [d], [], [])
240
+ for d in _ld_library_path_dirs()] # fallback, never an override
241
+ return out
242
+
243
+
244
+ def lib_search_dirs() -> List[str]:
245
+ """Every directory that may hold a dv-solve shared library, best first."""
246
+ return [d for inst in candidates() for d in inst.lib_dirs]
247
+
248
+
249
+ # ------------------------------------------------------------ file probing --
250
+
251
+
252
+ def _versioned_re(stem: str) -> Optional["re.Pattern[str]"]:
253
+ """Names of a numerically versioned library -- loadable, not linkable.
254
+
255
+ Numeric suffixes only, so ``libdv_solve.so.1.debug`` or a stray
256
+ ``libdv_solve.so.bak`` is never mistaken for the library.
257
+ """
258
+ system = platform.system()
259
+ if system == "Windows":
260
+ return None
261
+ if system == "Darwin":
262
+ return re.compile(r"^lib%s(\.\d+)+\.dylib$" % re.escape(stem))
263
+ return re.compile(r"^lib%s\.so(\.\d+)+$" % re.escape(stem))
264
+
265
+
266
+ def _is_file(path: str) -> bool:
267
+ """A real file, or a symlink that ends at one. ``isfile`` follows links,
268
+ so this rejects both directories and dangling symlinks."""
269
+ return os.path.isfile(path)
270
+
271
+
272
+ def _unversioned_in(d: str, stem: str) -> Optional[str]:
273
+ p = os.path.join(d, lib_filename(stem))
274
+ return p if _is_file(p) else None
275
+
276
+
277
+ def _versioned_in(d: str, stem: str) -> Optional[str]:
278
+ rx = _versioned_re(stem)
279
+ if rx is None or not os.path.isdir(d):
280
+ return None
281
+ hits = sorted((n for n in os.listdir(d) if rx.match(n)
282
+ and _is_file(os.path.join(d, n))), key=lambda n: (len(n), n))
283
+ return os.path.join(d, hits[0]) if hits else None
284
+
285
+
286
+ def library_in(inst: Installation, stem: str,
287
+ linkable: bool = False) -> Optional[str]:
288
+ """Library *stem* within *inst* only, or ``None``.
289
+
290
+ The unversioned name is sought across ALL of the installation's library
291
+ directories before any versioned name is considered, so that whenever the
292
+ installation can be linked, the loader opens the same file the linker
293
+ uses.
294
+ """
295
+ for d in inst.lib_dirs:
296
+ hit = _unversioned_in(d, stem)
297
+ if hit:
298
+ return hit
299
+ if linkable:
300
+ return None
301
+ for d in inst.lib_dirs:
302
+ hit = _versioned_in(d, stem)
303
+ if hit:
304
+ return hit
305
+ return None
306
+
307
+
308
+ def _incdirs_in(inst: Installation) -> Optional[List[str]]:
309
+ for candidate in inst.inc_sets:
310
+ for d in candidate:
311
+ if _is_file(os.path.join(d, _SENTINEL_HEADER)):
312
+ return [p for p in candidate if os.path.isdir(p)]
313
+ return None
314
+
315
+
316
+ def _svdirs_in(inst: Installation) -> Optional[List[str]]:
317
+ for d in inst.sv_dirs:
318
+ if _is_file(os.path.join(d, _SENTINEL_SV)):
319
+ return [d]
320
+ return None
321
+
322
+
323
+ # --------------------------------------------------------------- selection --
324
+
325
+
326
+ def select_installation() -> Optional[Installation]:
327
+ """The installation every artifact comes from, or ``None`` if none.
328
+
329
+ The override when set (usable or not); otherwise the first candidate
330
+ holding a loadable core library.
331
+ """
332
+ cands = candidates()
333
+ if cands and cands[0].kind == "override":
334
+ return cands[0]
335
+ for inst in cands:
336
+ if library_in(inst, "dv_solve") is not None:
337
+ return inst
338
+ return None
339
+
340
+
341
+ def find_library(stem: str, linkable: bool = False) -> Optional[str]:
342
+ """Absolute path to shared library *stem* in the selected installation,
343
+ or ``None``.
344
+
345
+ With ``linkable=True`` only the unversioned name is accepted, because that
346
+ is what ``-l<stem>`` resolves. ``None`` then means the SELECTED
347
+ installation cannot be linked -- a later candidate is never consulted
348
+ instead. Use :func:`require_library` for the diagnostic.
349
+ """
350
+ inst = select_installation()
351
+ return library_in(inst, stem, linkable) if inst is not None else None
352
+
353
+
354
+ def find_incdirs() -> Optional[List[str]]:
355
+ """The include set for compiling against dv-solve, or ``None``.
356
+
357
+ A set rather than a single directory because the installed layout needs
358
+ two entries: headers are staged under ``share/include/dv_solve/``
359
+ (CMake's ``DESTINATION include/dv_solve``), while dv-solve's own sources
360
+ and pssc's generated ``pssc_solve.c`` both use UNQUALIFIED includes
361
+ (``#include "dvs_ctx.h"``), which only resolve against the nested
362
+ directory. The base is kept alongside it for any consumer that writes
363
+ ``dv_solve/dvs_ctx.h``.
364
+
365
+ Validated by probing for a real header: an unbuilt checkout and a wheel
366
+ whose data files failed to stage both leave a plausible-looking directory
367
+ behind, and handing that to ``gcc -I`` defers the failure to a confusing
368
+ ``dvs_block_alloc.h: No such file or directory`` in generated code.
369
+
370
+ Taken from the selected installation only. With nothing selected (no
371
+ library anywhere) the first candidate holding headers answers, for
372
+ compile-only consumers.
373
+
374
+ The public API is ``dv_solve/dv_solve.h`` (or ``dv_solve.h`` through the
375
+ nested directory); the other headers are internal. They are prefixed
376
+ ``dvs_``, but are still kept out of the backend's include set: these
377
+ directories are for the SOLVER translation unit only.
378
+ """
379
+ inst = select_installation()
380
+ if inst is not None:
381
+ return _incdirs_in(inst)
382
+ for inst in candidates():
383
+ found = _incdirs_in(inst)
384
+ if found is not None:
385
+ return found
386
+ return None
387
+
388
+
389
+ def find_svdirs() -> Optional[List[str]]:
390
+ """The SystemVerilog package search path, or ``None``. Same selection
391
+ rule as :func:`find_incdirs`."""
392
+ inst = select_installation()
393
+ if inst is not None:
394
+ return _svdirs_in(inst)
395
+ for inst in candidates():
396
+ found = _svdirs_in(inst)
397
+ if found is not None:
398
+ return found
399
+ return None
400
+
401
+
402
+ # ------------------------------------------------------------- diagnostics --
403
+
404
+
405
+ def describe_search() -> str:
406
+ """A rendered search path, for embedding in diagnostics."""
407
+ return "\n".join(" - %s" % d for d in lib_search_dirs())
408
+
409
+
410
+ def missing_artifact_error(what: str, detail: str = "") -> RuntimeError:
411
+ """A uniform, actionable error for any missing native artifact."""
412
+ return RuntimeError(
413
+ "dv-solve: %s not found.%s\n"
414
+ "Searched:\n%s\n"
415
+ "Fixes:\n"
416
+ " - Install a binary wheel: pip install dv-solve\n"
417
+ " - Build from source (needs CMake + a C compiler):\n"
418
+ " cmake -S . -B build -G Ninja -DCMAKE_INSTALL_PREFIX=build\n"
419
+ " ninja -C build install\n"
420
+ " - Or point DVS_SOLVER_PATH at the install prefix / directory."
421
+ % (what, (" " + detail) if detail else "", describe_search()))
422
+
423
+
424
+ def _incomplete_error(inst: Installation, what: str, why: str) -> RuntimeError:
425
+ """The selected installation lacks *what*. Never resolved by looking
426
+ elsewhere -- that would pair artifacts from two solver builds."""
427
+ fix = (" - Complete that installation, or point DVS_SOLVER_PATH at a "
428
+ "complete one." if inst.kind == "override" else
429
+ " - Complete or remove that installation, or set DVS_SOLVER_PATH "
430
+ "to select a complete one.")
431
+ return RuntimeError(
432
+ "dv-solve: the selected installation (%s) has no %s.%s\n"
433
+ "Other installations are not consulted: every artifact must come "
434
+ "from the installation the solver library is loaded from.\n"
435
+ "Fixes:\n%s" % (inst.describe(), what, (" " + why) if why else "", fix))
436
+
437
+
438
+ def require_library(stem: str, linkable: bool = False) -> str:
439
+ """Like :func:`find_library` but raises an actionable ``RuntimeError``."""
440
+ inst = select_installation()
441
+ if inst is None:
442
+ raise missing_artifact_error(
443
+ "native library (%s)" % " / ".join(lib_patterns(stem)))
444
+ hit = library_in(inst, stem, linkable)
445
+ if hit is not None:
446
+ return hit
447
+ versioned = library_in(inst, stem) if linkable else None
448
+ if versioned is not None:
449
+ raise _incomplete_error(
450
+ inst, "linkable %s" % lib_filename(stem),
451
+ "It holds %s, which can be loaded but which -l%s cannot resolve. "
452
+ "Add the linker name alongside it (e.g. ln -s %s %s)."
453
+ % (versioned, stem, os.path.basename(versioned),
454
+ lib_filename(stem)))
455
+ raise _incomplete_error(inst, lib_filename(stem), "")
456
+
457
+
458
+ def require_incdirs() -> List[str]:
459
+ found = find_incdirs()
460
+ if found is not None:
461
+ return found
462
+ inst = select_installation()
463
+ if inst is None:
464
+ raise missing_artifact_error("C headers (%s)" % _SENTINEL_HEADER)
465
+ raise _incomplete_error(inst, "C headers (%s)" % _SENTINEL_HEADER, "")
466
+
467
+
468
+ def require_svdirs() -> List[str]:
469
+ found = find_svdirs()
470
+ if found is not None:
471
+ return found
472
+ inst = select_installation()
473
+ if inst is None:
474
+ raise missing_artifact_error("SV sources (%s)" % _SENTINEL_SV)
475
+ raise _incomplete_error(inst, "SV sources (%s)" % _SENTINEL_SV, "")
476
+
477
+
478
+ # --------------------------------------------------------- dv-solve-smt2 ----
479
+ #
480
+ # The SMT-LIB2 command-line tool is statically linked against the solver, so
481
+ # unlike the libraries it cannot be mismatched with the installation the
482
+ # Python API loads: any copy is self-contained. It still follows the same
483
+ # order, so that the answer is the one a user of THIS package expects.
484
+ #
485
+ # 1. The override (DVS_SOLVER_PATH), alone, as for the libraries: the root
486
+ # itself or its bin/.
487
+ # 2. The installed wheel's own copy. pip puts it in the environment's
488
+ # scripts directory (bin/, or Scripts\ on Windows), outside the package,
489
+ # so it is found through the distribution's RECORD -- which works for
490
+ # venv, --user and system installs alike. Only a distribution whose
491
+ # RECORD holds THIS package's __init__.py counts, so a wheel installed
492
+ # elsewhere cannot answer for a source checkout.
493
+ # 3. A built source checkout: <build>/bin (the install destination), then
494
+ # <build> itself (CMake's link output).
495
+ # 4. PATH, as a last resort.
496
+
497
+ SMT2_NAME = "dv-solve-smt2"
498
+
499
+
500
+ def smt2_filename() -> str:
501
+ """The platform's file name for the SMT-LIB2 tool."""
502
+ if platform.system() == "Windows":
503
+ return SMT2_NAME + ".exe"
504
+ return SMT2_NAME
505
+
506
+
507
+ def _is_exe(path: str) -> bool:
508
+ return _is_file(path) and os.access(path, os.X_OK)
509
+
510
+
511
+ def _dist_smt2() -> Optional[str]:
512
+ """The executable recorded by the distribution that owns this package."""
513
+ try:
514
+ from importlib import metadata
515
+ dist = metadata.distribution("dv-solve")
516
+ except Exception:
517
+ return None
518
+ files = dist.files or []
519
+ init = os.path.join(_pkg_dir(), "__init__.py")
520
+ owns = any(f.name == "__init__.py" and f.parent.name == "dv_solve"
521
+ and os.path.abspath(str(dist.locate_file(f))) == init
522
+ for f in files)
523
+ if not owns:
524
+ return None
525
+ name = smt2_filename()
526
+ for f in files:
527
+ if f.name == name:
528
+ p = os.path.abspath(str(dist.locate_file(f)))
529
+ if _is_exe(p):
530
+ return p
531
+ return None
532
+
533
+
534
+ def smt2_search_paths() -> List[str]:
535
+ """Every location probed for the SMT-LIB2 tool, best first (for
536
+ diagnostics; the RECORD and PATH lookups are named, not expanded)."""
537
+ name, j = smt2_filename(), os.path.join
538
+ root = override_root()
539
+ if root:
540
+ return [j(root, name), j(root, "bin", name)]
541
+ out = ["the dv-solve distribution's installed scripts (RECORD)"]
542
+ for bd in (j(_src_root(), n) for n in _BUILD_DIR_NAMES):
543
+ out += [j(bd, "bin", name), j(bd, name)]
544
+ out.append("PATH")
545
+ return out
546
+
547
+
548
+ def find_smt2_exe() -> Optional[str]:
549
+ """Absolute path to ``dv-solve-smt2``, or ``None``. See above for order."""
550
+ name, j = smt2_filename(), os.path.join
551
+ root = override_root()
552
+ if root:
553
+ for p in (j(root, name), j(root, "bin", name)):
554
+ if _is_exe(p):
555
+ return os.path.abspath(p)
556
+ return None # the override is terminal
557
+ hit = _dist_smt2()
558
+ if hit:
559
+ return hit
560
+ for bd in (j(_src_root(), n) for n in _BUILD_DIR_NAMES):
561
+ for p in (j(bd, "bin", name), j(bd, name)):
562
+ if _is_exe(p):
563
+ return os.path.abspath(p)
564
+ import shutil
565
+ # The full file name, not the stem: on Windows which() would otherwise
566
+ # append the PATHEXT spelling (".EXE") rather than the file's own.
567
+ hit = shutil.which(name)
568
+ return os.path.abspath(hit) if hit else None
569
+
570
+
571
+ def require_smt2_exe() -> str:
572
+ """Like :func:`find_smt2_exe` but raises an actionable ``RuntimeError``."""
573
+ hit = find_smt2_exe()
574
+ if hit is not None:
575
+ return hit
576
+ raise smt2_missing_error()
577
+
578
+
579
+ def smt2_missing_error() -> RuntimeError:
580
+ """The error for a missing ``dv-solve-smt2``, naming where it looked."""
581
+ detail = ""
582
+ if override_root():
583
+ detail = "%s is set, so no other location was searched." % _override_var()
584
+ return RuntimeError(
585
+ "dv-solve: %s not found.%s\n"
586
+ "Searched:\n%s\n"
587
+ "Fixes:\n"
588
+ " - Install a binary wheel: pip install dv-solve\n"
589
+ " - Build from source (needs CMake + a C compiler):\n"
590
+ " cmake -S . -B build -G Ninja -DCMAKE_INSTALL_PREFIX=build\n"
591
+ " ninja -C build install\n"
592
+ " - Or point DVS_SOLVER_PATH at a directory or prefix holding it."
593
+ % (smt2_filename(), (" " + detail) if detail else "",
594
+ "\n".join(" - %s" % p for p in smt2_search_paths())))