nab-resolver 0.0.1__py3-none-any.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.
- nab_resolver/__init__.py +1 -0
- nab_resolver/conflict.py +491 -0
- nab_resolver/decide.py +138 -0
- nab_resolver/errors.py +36 -0
- nab_resolver/incompat_index.py +119 -0
- nab_resolver/partial_solution.py +394 -0
- nab_resolver/propagate.py +156 -0
- nab_resolver/py.typed +0 -0
- nab_resolver/ranges.py +419 -0
- nab_resolver/report.py +179 -0
- nab_resolver/resolver.py +456 -0
- nab_resolver/result.py +79 -0
- nab_resolver/root.py +32 -0
- nab_resolver/types.py +257 -0
- nab_resolver-0.0.1.dist-info/METADATA +39 -0
- nab_resolver-0.0.1.dist-info/RECORD +18 -0
- nab_resolver-0.0.1.dist-info/WHEEL +4 -0
- nab_resolver-0.0.1.dist-info/licenses/LICENSE +21 -0
nab_resolver/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""PubGrub dependency resolver."""
|
nab_resolver/conflict.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""Conflict resolution and backtracking for the PubGrub resolver.
|
|
2
|
+
|
|
3
|
+
Owns the conflict-resolution loop, the most-recent-satisfier
|
|
4
|
+
search, the always-learn force-resolution gate, the targeted
|
|
5
|
+
backtrack queue, and the catastrophic restart handler.
|
|
6
|
+
|
|
7
|
+
Reference: https://github.com/dart-lang/pub/blob/master/doc/solver.md#conflict-resolution
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from .errors import ResolutionError
|
|
15
|
+
from .incompat_index import add_incompatibility
|
|
16
|
+
from .partial_solution import PartialSolution
|
|
17
|
+
from .report import format_error, prior_cause
|
|
18
|
+
from .root import ROOT
|
|
19
|
+
from .types import Incompatibility, IncompatibilityCause
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .partial_solution import Assignment
|
|
23
|
+
from .resolver import Resolver
|
|
24
|
+
from .types import Term
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"apply_targeted_backtrack",
|
|
29
|
+
"conflict_resolution",
|
|
30
|
+
"find_most_recent_satisfier",
|
|
31
|
+
"force_targeted_backtrack",
|
|
32
|
+
"is_terminal_incompatibility",
|
|
33
|
+
"iterate_force_resolution",
|
|
34
|
+
"maybe_restart",
|
|
35
|
+
"maybe_targeted_backtrack",
|
|
36
|
+
"recompute_previous_level",
|
|
37
|
+
"try_force_resolution_step",
|
|
38
|
+
"update_culprit_counts",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Soundness gate for try_force_resolution_step: a single-term
|
|
43
|
+
# incompatibility must resolve to >= 2 terms to keep the eliminated
|
|
44
|
+
# package's conditioning.
|
|
45
|
+
_SINGLE_TERM = 1
|
|
46
|
+
_MIN_RESOLVED_TERMS = 2
|
|
47
|
+
|
|
48
|
+
# Lowest decision level a targeted-backtrack can land above without
|
|
49
|
+
# removing ROOT (decided at level 1 in _add_root_requirements).
|
|
50
|
+
_TARGETED_BT_MIN_LEVEL = 2
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def conflict_resolution(
|
|
54
|
+
resolver: Resolver[Any, Any],
|
|
55
|
+
conflicting_incompatibility: Incompatibility[Any, Any],
|
|
56
|
+
) -> Incompatibility[Any, Any]:
|
|
57
|
+
"""Learn a new incompatibility and backjump to the appropriate level.
|
|
58
|
+
|
|
59
|
+
Implements PubGrub's conflict resolution algorithm: resolve
|
|
60
|
+
backwards through the assignment trail combining incompatibilities
|
|
61
|
+
until the learned clause has at most one term at the current
|
|
62
|
+
decision level.
|
|
63
|
+
|
|
64
|
+
Reference: https://github.com/dart-lang/pub/blob/master/doc/solver.md#conflict-resolution
|
|
65
|
+
"""
|
|
66
|
+
current_incompatibility = conflicting_incompatibility
|
|
67
|
+
is_derived = False
|
|
68
|
+
|
|
69
|
+
while True:
|
|
70
|
+
if is_terminal_incompatibility(current_incompatibility):
|
|
71
|
+
# Unreachable in practice: the backjump_target == 0 check below
|
|
72
|
+
# preempts this path. Kept as a safety net.
|
|
73
|
+
raise ResolutionError( # pragma: no cover
|
|
74
|
+
format_error(current_incompatibility),
|
|
75
|
+
incompatibility=current_incompatibility,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
satisfier_result = find_most_recent_satisfier(resolver, current_incompatibility)
|
|
79
|
+
most_recent_satisfier = satisfier_result[0]
|
|
80
|
+
most_recent_satisfier_term = satisfier_result[1]
|
|
81
|
+
previous_satisfier_level = satisfier_result[2]
|
|
82
|
+
|
|
83
|
+
can_backjump = (
|
|
84
|
+
most_recent_satisfier.is_decision
|
|
85
|
+
or previous_satisfier_level != most_recent_satisfier.decision_level
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if can_backjump:
|
|
89
|
+
(
|
|
90
|
+
current_incompatibility,
|
|
91
|
+
most_recent_satisfier,
|
|
92
|
+
most_recent_satisfier_term,
|
|
93
|
+
previous_satisfier_level,
|
|
94
|
+
forced_any,
|
|
95
|
+
) = iterate_force_resolution(
|
|
96
|
+
resolver,
|
|
97
|
+
current_incompatibility,
|
|
98
|
+
most_recent_satisfier,
|
|
99
|
+
most_recent_satisfier_term,
|
|
100
|
+
previous_satisfier_level,
|
|
101
|
+
)
|
|
102
|
+
if forced_any:
|
|
103
|
+
is_derived = True
|
|
104
|
+
|
|
105
|
+
resolver.observer.on_conflict_step(
|
|
106
|
+
current_incompatibility,
|
|
107
|
+
satisfier_package=most_recent_satisfier.package,
|
|
108
|
+
satisfier_is_decision=most_recent_satisfier.is_decision,
|
|
109
|
+
satisfier_level=most_recent_satisfier.decision_level,
|
|
110
|
+
previous_level=previous_satisfier_level,
|
|
111
|
+
can_backjump=can_backjump,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if can_backjump:
|
|
115
|
+
if is_derived:
|
|
116
|
+
add_incompatibility(resolver, current_incompatibility)
|
|
117
|
+
resolver.stats.incompatibilities_learned += 1
|
|
118
|
+
resolver.observer.on_learned(current_incompatibility)
|
|
119
|
+
|
|
120
|
+
backjump_target = previous_satisfier_level
|
|
121
|
+
if (
|
|
122
|
+
most_recent_satisfier.is_decision
|
|
123
|
+
and backjump_target >= most_recent_satisfier.decision_level
|
|
124
|
+
):
|
|
125
|
+
backjump_target = most_recent_satisfier.decision_level - 1
|
|
126
|
+
|
|
127
|
+
backjump_target = min(backjump_target, resolver.solution.decision_level)
|
|
128
|
+
backjump_target = max(backjump_target, 0)
|
|
129
|
+
|
|
130
|
+
if backjump_target == 0:
|
|
131
|
+
raise ResolutionError(
|
|
132
|
+
format_error(current_incompatibility),
|
|
133
|
+
incompatibility=current_incompatibility,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
from_level = resolver.solution.decision_level
|
|
137
|
+
resolver.solution.backtrack(backjump_target)
|
|
138
|
+
resolver.stats.backjumps += 1
|
|
139
|
+
resolver.observer.on_backjump(from_level, backjump_target)
|
|
140
|
+
|
|
141
|
+
# Count only the "affected" package so it gets decided first
|
|
142
|
+
# after restart and its dependencies constrain the culprit.
|
|
143
|
+
affected_package = most_recent_satisfier.package
|
|
144
|
+
resolver.stats.package_conflict_counts[affected_package] += 1
|
|
145
|
+
|
|
146
|
+
update_culprit_counts(
|
|
147
|
+
resolver,
|
|
148
|
+
current_incompatibility,
|
|
149
|
+
affected_package,
|
|
150
|
+
most_recent_satisfier,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return current_incompatibility
|
|
154
|
+
|
|
155
|
+
# Can't backjump yet: resolve with the satisfier's cause.
|
|
156
|
+
assert most_recent_satisfier.cause is not None
|
|
157
|
+
assert most_recent_satisfier_term is not None
|
|
158
|
+
is_derived = True
|
|
159
|
+
|
|
160
|
+
resolved_terms = prior_cause(
|
|
161
|
+
current_incompatibility,
|
|
162
|
+
most_recent_satisfier.cause,
|
|
163
|
+
most_recent_satisfier_term.package,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
current_incompatibility = Incompatibility(
|
|
167
|
+
resolved_terms,
|
|
168
|
+
cause=IncompatibilityCause.DERIVED,
|
|
169
|
+
cause_left=current_incompatibility,
|
|
170
|
+
cause_right=most_recent_satisfier.cause,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def update_culprit_counts(
|
|
175
|
+
resolver: Resolver[Any, Any],
|
|
176
|
+
incompatibility: Incompatibility[Any, Any],
|
|
177
|
+
affected_package: Any,
|
|
178
|
+
satisfier: Assignment[Any, Any],
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Credit non-affected packages in a learned clause as culprits.
|
|
181
|
+
|
|
182
|
+
Modelled on uv's ConflictTracker (PR #9843). Every non-affected,
|
|
183
|
+
non-root package in the clause is a culprit; for single-term NO_VERSIONS
|
|
184
|
+
clauses we walk the satisfier's cause chain instead. When a culprit
|
|
185
|
+
crosses ``CULPRIT_THRESHOLD`` it gets queued for targeted backtrack.
|
|
186
|
+
"""
|
|
187
|
+
culprit_packages: set[Any] = set()
|
|
188
|
+
for term in incompatibility.terms:
|
|
189
|
+
package = term.package
|
|
190
|
+
if package is ROOT or package == affected_package:
|
|
191
|
+
continue
|
|
192
|
+
culprit_packages.add(package)
|
|
193
|
+
|
|
194
|
+
# Single-term NO_VERSIONS clauses carry the antecedent decisions only
|
|
195
|
+
# via the satisfier's cause; without this, those decisions go uncredited.
|
|
196
|
+
if len(incompatibility.terms) == 1 and satisfier.cause is not None:
|
|
197
|
+
for term in satisfier.cause.terms:
|
|
198
|
+
package = term.package
|
|
199
|
+
if package is ROOT or package == affected_package:
|
|
200
|
+
continue
|
|
201
|
+
culprit_packages.add(package)
|
|
202
|
+
|
|
203
|
+
threshold = resolver.CULPRIT_THRESHOLD
|
|
204
|
+
for package in culprit_packages:
|
|
205
|
+
resolver.stats.package_culprit_counts[package] += 1
|
|
206
|
+
count = resolver.stats.package_culprit_counts[package]
|
|
207
|
+
if (
|
|
208
|
+
count >= threshold
|
|
209
|
+
and count % threshold == 0
|
|
210
|
+
and package not in resolver.pending_targeted_backtrack
|
|
211
|
+
):
|
|
212
|
+
resolver.pending_targeted_backtrack.append(package)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def iterate_force_resolution(
|
|
216
|
+
resolver: Resolver[Any, Any],
|
|
217
|
+
incompatibility: Incompatibility[Any, Any],
|
|
218
|
+
satisfier: Assignment[Any, Any],
|
|
219
|
+
satisfier_term: Term[Any, Any],
|
|
220
|
+
previous_satisfier_level: int,
|
|
221
|
+
) -> tuple[
|
|
222
|
+
Incompatibility[Any, Any],
|
|
223
|
+
Assignment[Any, Any],
|
|
224
|
+
Term[Any, Any],
|
|
225
|
+
int,
|
|
226
|
+
bool,
|
|
227
|
+
]:
|
|
228
|
+
"""Iterate :func:`try_force_resolution_step` while it succeeds.
|
|
229
|
+
|
|
230
|
+
Returns the (possibly-resolved) incompatibility, refreshed satisfier,
|
|
231
|
+
satisfier term, previous-satisfier level, and a ``forced_any`` flag
|
|
232
|
+
indicating whether at least one resolution step happened. Stops when
|
|
233
|
+
``try_force_resolution_step`` declines (returns ``None``).
|
|
234
|
+
"""
|
|
235
|
+
forced_any = False
|
|
236
|
+
|
|
237
|
+
while True:
|
|
238
|
+
forced = try_force_resolution_step(
|
|
239
|
+
resolver, incompatibility, satisfier, satisfier_term
|
|
240
|
+
)
|
|
241
|
+
if forced is None:
|
|
242
|
+
break
|
|
243
|
+
forced_any = True
|
|
244
|
+
incompatibility = forced
|
|
245
|
+
(
|
|
246
|
+
satisfier,
|
|
247
|
+
satisfier_term,
|
|
248
|
+
previous_satisfier_level,
|
|
249
|
+
) = find_most_recent_satisfier(resolver, forced)
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
incompatibility,
|
|
253
|
+
satisfier,
|
|
254
|
+
satisfier_term,
|
|
255
|
+
previous_satisfier_level,
|
|
256
|
+
forced_any,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def try_force_resolution_step(
|
|
261
|
+
resolver: Resolver[Any, Any],
|
|
262
|
+
incompatibility: Incompatibility[Any, Any],
|
|
263
|
+
satisfier: Assignment[Any, Any],
|
|
264
|
+
satisfier_term: Term[Any, Any],
|
|
265
|
+
) -> Incompatibility[Any, Any] | None:
|
|
266
|
+
"""Resolve a single-term NO_VERSIONS clause once with a soundness gate.
|
|
267
|
+
|
|
268
|
+
Standard PubGrub backjumps to root for single-term clauses, losing the
|
|
269
|
+
supporting decisions; resolving once with the satisfier's cause exposes
|
|
270
|
+
them so later propagation can skip the bad decision.
|
|
271
|
+
|
|
272
|
+
Returns ``None`` when the resolved clause collapses to <2 terms (the
|
|
273
|
+
eliminated package's conditioning would be lost) or when it is not
|
|
274
|
+
assert-eligible.
|
|
275
|
+
"""
|
|
276
|
+
if (
|
|
277
|
+
satisfier.is_decision
|
|
278
|
+
or len(incompatibility.terms) != _SINGLE_TERM
|
|
279
|
+
or satisfier.cause is None
|
|
280
|
+
):
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
resolved_terms = prior_cause(
|
|
284
|
+
incompatibility, satisfier.cause, satisfier_term.package
|
|
285
|
+
)
|
|
286
|
+
if len(resolved_terms) < _MIN_RESOLVED_TERMS:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
forced = Incompatibility(
|
|
290
|
+
resolved_terms,
|
|
291
|
+
cause=IncompatibilityCause.DERIVED,
|
|
292
|
+
cause_left=incompatibility,
|
|
293
|
+
cause_right=satisfier.cause,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
forced_recent, _, forced_prev_level = find_most_recent_satisfier(resolver, forced)
|
|
297
|
+
forced_can_backjump = (
|
|
298
|
+
forced_recent.is_decision or forced_prev_level != forced_recent.decision_level
|
|
299
|
+
)
|
|
300
|
+
if not forced_can_backjump:
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
return forced
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def find_most_recent_satisfier(
|
|
307
|
+
resolver: Resolver[Any, Any], incompatibility: Incompatibility[Any, Any]
|
|
308
|
+
) -> tuple[Assignment[Any, Any], Term[Any, Any], int]:
|
|
309
|
+
"""Find the most recently assigned satisfier across all terms.
|
|
310
|
+
|
|
311
|
+
Returns ``(satisfier, term, previous_satisfier_level)``. The previous
|
|
312
|
+
level is the highest among the other terms' satisfiers and bounds how
|
|
313
|
+
far back the resolver can jump. Refined when the satisfier is partial
|
|
314
|
+
(earlier assignments also contributed).
|
|
315
|
+
"""
|
|
316
|
+
most_recent: Assignment[Any, Any] | None = None
|
|
317
|
+
most_recent_term: Term[Any, Any] | None = None
|
|
318
|
+
previous_level = 1 # root's level
|
|
319
|
+
|
|
320
|
+
for term in incompatibility.terms:
|
|
321
|
+
satisfier = resolver.solution.satisfier(term)
|
|
322
|
+
if satisfier is None: # pragma: no cover
|
|
323
|
+
unreachable = (
|
|
324
|
+
f"Bug: no satisfier for {term!r} in a satisfied incompatibility"
|
|
325
|
+
)
|
|
326
|
+
raise RuntimeError(unreachable)
|
|
327
|
+
satisfier_index = satisfier.trail_index
|
|
328
|
+
if most_recent is None or (satisfier_index > most_recent.trail_index):
|
|
329
|
+
if most_recent is not None:
|
|
330
|
+
previous_level = max(previous_level, most_recent.decision_level)
|
|
331
|
+
most_recent = satisfier
|
|
332
|
+
most_recent_term = term
|
|
333
|
+
else:
|
|
334
|
+
previous_level = max(previous_level, satisfier.decision_level)
|
|
335
|
+
|
|
336
|
+
if most_recent is None: # pragma: no cover
|
|
337
|
+
unreachable = "Bug: no satisfiers in a satisfied incompatibility"
|
|
338
|
+
raise RuntimeError(unreachable)
|
|
339
|
+
assert most_recent_term is not None
|
|
340
|
+
|
|
341
|
+
if not resolver.solution.satisfier_is_sole(most_recent, most_recent_term):
|
|
342
|
+
previous_level = recompute_previous_level(
|
|
343
|
+
resolver, most_recent, most_recent_term, previous_level
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return most_recent, most_recent_term, previous_level
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def recompute_previous_level(
|
|
350
|
+
resolver: Resolver[Any, Any],
|
|
351
|
+
satisfier: Assignment[Any, Any],
|
|
352
|
+
satisfier_term: Term[Any, Any],
|
|
353
|
+
current_previous_level: int,
|
|
354
|
+
) -> int:
|
|
355
|
+
"""Refine previous_level when the satisfier is partial.
|
|
356
|
+
|
|
357
|
+
Isolates the satisfier's own contribution (from its cause), subtracts
|
|
358
|
+
it from the term, and folds the remainder's satisfier level in.
|
|
359
|
+
"""
|
|
360
|
+
if satisfier.cause is None:
|
|
361
|
+
return current_previous_level
|
|
362
|
+
|
|
363
|
+
cause_term = None
|
|
364
|
+
for term in satisfier.cause.terms:
|
|
365
|
+
if term.package == satisfier_term.package:
|
|
366
|
+
cause_term = term
|
|
367
|
+
break
|
|
368
|
+
if cause_term is None:
|
|
369
|
+
return current_previous_level
|
|
370
|
+
|
|
371
|
+
individual = cause_term.negate()
|
|
372
|
+
remainder = satisfier_term.intersect(individual.negate())
|
|
373
|
+
if remainder is None or remainder.constraint.is_empty:
|
|
374
|
+
return current_previous_level
|
|
375
|
+
|
|
376
|
+
remainder_satisfier = resolver.solution.satisfier(remainder)
|
|
377
|
+
if remainder_satisfier is not None:
|
|
378
|
+
return max(current_previous_level, remainder_satisfier.decision_level)
|
|
379
|
+
return current_previous_level
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def is_terminal_incompatibility(incompatibility: Incompatibility[Any, Any]) -> bool:
|
|
383
|
+
"""Check whether this incompatibility proves resolution is impossible."""
|
|
384
|
+
if not incompatibility.terms:
|
|
385
|
+
return True
|
|
386
|
+
return all(term.package is ROOT for term in incompatibility.terms)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def maybe_targeted_backtrack(resolver: Resolver[Any, Any]) -> Any | None:
|
|
390
|
+
"""Run :func:`apply_targeted_backtrack` if the gate is open.
|
|
391
|
+
|
|
392
|
+
Gate: pending non-empty AND total conflicts past
|
|
393
|
+
``TARGETED_BT_MIN_CONFLICTS``. Pending culprits are kept across rounds.
|
|
394
|
+
"""
|
|
395
|
+
if (
|
|
396
|
+
resolver.pending_targeted_backtrack
|
|
397
|
+
and resolver.stats.conflicts >= resolver.TARGETED_BT_MIN_CONFLICTS
|
|
398
|
+
):
|
|
399
|
+
return apply_targeted_backtrack(resolver)
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def maybe_restart(
|
|
404
|
+
resolver: Resolver[Any, Any],
|
|
405
|
+
restart_threshold: int,
|
|
406
|
+
restarts_remaining: int,
|
|
407
|
+
) -> tuple[int, int, bool]:
|
|
408
|
+
"""Restart the solver if any package crossed ``restart_threshold``.
|
|
409
|
+
|
|
410
|
+
Returns ``(new_threshold, new_remaining, restarted)``. Preserves
|
|
411
|
+
``incompatibilities`` and ``package_conflict_counts`` across restart.
|
|
412
|
+
"""
|
|
413
|
+
if restarts_remaining <= 0:
|
|
414
|
+
return restart_threshold, restarts_remaining, False
|
|
415
|
+
|
|
416
|
+
max_count = max(resolver.stats.package_conflict_counts.values(), default=0)
|
|
417
|
+
if max_count < restart_threshold:
|
|
418
|
+
return restart_threshold, restarts_remaining, False
|
|
419
|
+
|
|
420
|
+
resolver.stats.restarts += 1
|
|
421
|
+
resolver.solution = PartialSolution(range_type=resolver.range_type)
|
|
422
|
+
resolver.solution.decide(ROOT, resolver.root_version)
|
|
423
|
+
resolver.pending_targeted_backtrack.clear()
|
|
424
|
+
resolver.stats.targeted_backtracks = 0
|
|
425
|
+
|
|
426
|
+
return restart_threshold * 2, restarts_remaining - 1, True
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def force_targeted_backtrack(
|
|
430
|
+
resolver: Resolver[Any, Any], packages: list[Any]
|
|
431
|
+
) -> Any | None:
|
|
432
|
+
"""Apply a targeted back-track without waiting for the normal gate.
|
|
433
|
+
|
|
434
|
+
Used when the provider supplies direct evidence that the named
|
|
435
|
+
packages are culprits. Bumps each package's culprit count past the
|
|
436
|
+
dominant-culprit threshold, queues it, and applies immediately.
|
|
437
|
+
|
|
438
|
+
Returns the back-jumped package, or ``None`` if the back-track did
|
|
439
|
+
not move the decision level.
|
|
440
|
+
"""
|
|
441
|
+
if not packages:
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
threshold = resolver.CULPRIT_THRESHOLD
|
|
445
|
+
for package in packages:
|
|
446
|
+
current = resolver.stats.package_culprit_counts[package]
|
|
447
|
+
if current < threshold:
|
|
448
|
+
resolver.stats.package_culprit_counts[package] = threshold
|
|
449
|
+
if package not in resolver.pending_targeted_backtrack:
|
|
450
|
+
resolver.pending_targeted_backtrack.append(package)
|
|
451
|
+
|
|
452
|
+
return apply_targeted_backtrack(resolver)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def apply_targeted_backtrack(resolver: Resolver[Any, Any]) -> Any | None:
|
|
456
|
+
"""Backtrack to before the earliest pending-culprit assignment.
|
|
457
|
+
|
|
458
|
+
Picks the smallest decision-level among queued culprits' first assignments
|
|
459
|
+
(decision OR derivation; derivations count too so propagated culprits in
|
|
460
|
+
cluster conflicts still trigger a backtrack) and jumps to one before it.
|
|
461
|
+
Capped at ``MAX_TARGETED_BACKTRACKS`` per restart segment.
|
|
462
|
+
|
|
463
|
+
Level-1 assignments are skipped to preserve ROOT.
|
|
464
|
+
"""
|
|
465
|
+
if resolver.stats.targeted_backtracks >= resolver.MAX_TARGETED_BACKTRACKS:
|
|
466
|
+
resolver.pending_targeted_backtrack.clear()
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
target_level = resolver.solution.decision_level
|
|
470
|
+
triggering_package: Any | None = None
|
|
471
|
+
|
|
472
|
+
for package in resolver.pending_targeted_backtrack:
|
|
473
|
+
package_entries = resolver.solution.assignments_for(package)
|
|
474
|
+
# Use the package's earliest non-level-1 assignment: jumping
|
|
475
|
+
# further than that would just undo unrelated work.
|
|
476
|
+
for assignment in package_entries:
|
|
477
|
+
if assignment.decision_level < _TARGETED_BT_MIN_LEVEL:
|
|
478
|
+
continue
|
|
479
|
+
candidate = assignment.decision_level - 1
|
|
480
|
+
if candidate < target_level:
|
|
481
|
+
target_level = candidate
|
|
482
|
+
triggering_package = package
|
|
483
|
+
break
|
|
484
|
+
|
|
485
|
+
resolver.pending_targeted_backtrack.clear()
|
|
486
|
+
if triggering_package is None or target_level >= resolver.solution.decision_level:
|
|
487
|
+
return None
|
|
488
|
+
|
|
489
|
+
resolver.solution.backtrack(target_level)
|
|
490
|
+
resolver.stats.targeted_backtracks += 1
|
|
491
|
+
return triggering_package
|
nab_resolver/decide.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Decision making for the PubGrub resolver.
|
|
2
|
+
|
|
3
|
+
Picks the next undecided package, asks the provider for a version,
|
|
4
|
+
and records ``NO_VERSIONS`` clauses or constraint clauses as needed.
|
|
5
|
+
|
|
6
|
+
Reference: https://github.com/dart-lang/pub/blob/master/doc/solver.md#decision-making
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from .incompat_index import add_incompatibility
|
|
14
|
+
from .root import ROOT
|
|
15
|
+
from .types import Incompatibility, IncompatibilityCause, Term
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .resolver import Resolver
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"absorb_pending_clauses",
|
|
22
|
+
"choose_package_to_decide",
|
|
23
|
+
"choose_version",
|
|
24
|
+
"inject_constraint",
|
|
25
|
+
"record_no_versions",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def choose_package_to_decide(resolver: Resolver[Any, Any]) -> Any | None:
|
|
30
|
+
"""Choose the next undecided package, or None if all decided.
|
|
31
|
+
|
|
32
|
+
Prefers ``is_ready`` packages so resolution keeps making progress while
|
|
33
|
+
other listings/metadata are still in flight.
|
|
34
|
+
"""
|
|
35
|
+
undecided = resolver.solution.undecided_packages()
|
|
36
|
+
undecided.discard(ROOT)
|
|
37
|
+
if not undecided:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
conflict_counts = resolver.stats.package_conflict_counts
|
|
41
|
+
culprit_counts = resolver.stats.package_culprit_counts
|
|
42
|
+
get_range = resolver.solution.get
|
|
43
|
+
any_range = resolver.range_type.full()
|
|
44
|
+
prioritize = resolver.provider.prioritize
|
|
45
|
+
is_ready = resolver.provider.is_ready
|
|
46
|
+
tiebreak_cache = resolver.tiebreak_cache
|
|
47
|
+
root_order = resolver.root_package_order
|
|
48
|
+
|
|
49
|
+
def sort_key(package: Any) -> tuple[Any, ...]:
|
|
50
|
+
priority = prioritize(
|
|
51
|
+
package,
|
|
52
|
+
get_range(package) or any_range,
|
|
53
|
+
conflict_counts,
|
|
54
|
+
culprit_counts,
|
|
55
|
+
)
|
|
56
|
+
ready_penalty = 0 if is_ready(package) else 1
|
|
57
|
+
tiebreak = tiebreak_cache.get(package)
|
|
58
|
+
if tiebreak is None:
|
|
59
|
+
tiebreak = root_order.get(package)
|
|
60
|
+
if tiebreak is None:
|
|
61
|
+
tiebreak = (1, 0, str(package))
|
|
62
|
+
tiebreak_cache[package] = tiebreak
|
|
63
|
+
return (ready_penalty, priority, tiebreak)
|
|
64
|
+
|
|
65
|
+
return min(undecided, key=sort_key)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def inject_constraint(resolver: Resolver[Any, Any], package: Any) -> bool:
|
|
69
|
+
"""Inject a CONSTRAINT incompatibility for a package, if applicable.
|
|
70
|
+
|
|
71
|
+
Lazy: only fires the first time the package is about to be decided.
|
|
72
|
+
Returns True if an incompatibility was injected (caller re-propagates).
|
|
73
|
+
"""
|
|
74
|
+
if package not in resolver.constraints:
|
|
75
|
+
return False
|
|
76
|
+
if package in resolver.injected_constraints:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
resolver.injected_constraints.add(package)
|
|
80
|
+
complement = ~resolver.constraints[package]
|
|
81
|
+
if complement.is_empty:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
add_incompatibility(
|
|
85
|
+
resolver,
|
|
86
|
+
Incompatibility(
|
|
87
|
+
[Term(package, complement, positive=True)],
|
|
88
|
+
cause=IncompatibilityCause.CONSTRAINT,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def choose_version(resolver: Resolver[Any, Any], package: Any) -> Any | None:
|
|
95
|
+
"""Ask the provider to pick a version within the allowed range."""
|
|
96
|
+
current_range = resolver.solution.get(package) or resolver.range_type.full()
|
|
97
|
+
resolver.provider.receive_partial_solution_hint(
|
|
98
|
+
resolver.solution.positive_ranges(),
|
|
99
|
+
resolver.solution.decisions(),
|
|
100
|
+
)
|
|
101
|
+
return resolver.provider.choose_version(package, current_range)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def absorb_pending_clauses(resolver: Resolver[Any, Any]) -> bool:
|
|
105
|
+
"""Drain provider-queued incompatibilities into the formula.
|
|
106
|
+
|
|
107
|
+
Look-ahead providers push binary clauses like
|
|
108
|
+
``{candidate==v, blocking_decision==w}`` instead of relying on the
|
|
109
|
+
broader ``NO_VERSIONS`` clause. Returns True so the caller can suppress
|
|
110
|
+
the default ``NO_VERSIONS`` clause this turn.
|
|
111
|
+
"""
|
|
112
|
+
clauses = list(resolver.provider.consume_pending_clauses())
|
|
113
|
+
for incompatibility in clauses:
|
|
114
|
+
add_incompatibility(resolver, incompatibility)
|
|
115
|
+
return bool(clauses)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def record_no_versions(
|
|
119
|
+
resolver: Resolver[Any, Any], package: Any, *, had_pending: bool
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Add the default ``NO_VERSIONS`` clause for ``package``.
|
|
122
|
+
|
|
123
|
+
Skipped when the provider already supplied context-aware clauses;
|
|
124
|
+
otherwise the broad clause would persist past the backjump that lifts
|
|
125
|
+
the supporting decisions.
|
|
126
|
+
"""
|
|
127
|
+
if had_pending:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
current_range = resolver.solution.get(package) or resolver.range_type.full()
|
|
131
|
+
resolver.observer.on_no_versions(package, current_range)
|
|
132
|
+
add_incompatibility(
|
|
133
|
+
resolver,
|
|
134
|
+
Incompatibility(
|
|
135
|
+
[Term(package, current_range, positive=True)],
|
|
136
|
+
cause=IncompatibilityCause.NO_VERSIONS,
|
|
137
|
+
),
|
|
138
|
+
)
|
nab_resolver/errors.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Resolution error types.
|
|
2
|
+
|
|
3
|
+
Defines the exception raised when the resolver proves that no valid
|
|
4
|
+
solution exists, along with the derivation tree for error reporting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .types import Incompatibility
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ResolutionError",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ResolutionError(Exception):
|
|
20
|
+
"""Resolution failed: no valid solution exists.
|
|
21
|
+
|
|
22
|
+
The ``incompatibility`` attribute holds the root incompatibility
|
|
23
|
+
whose derivation tree explains why resolution failed. Walk
|
|
24
|
+
``cause_left`` and ``cause_right`` to trace the full proof.
|
|
25
|
+
|
|
26
|
+
Reference: https://github.com/dart-lang/pub/blob/master/doc/solver.md#error-reporting
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
message: str,
|
|
32
|
+
incompatibility: Incompatibility[Any, Any] | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Create a resolution error with an optional incompatibility proof."""
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.incompatibility = incompatibility
|