Skip to content

Commit a9ed567

Browse files
committed
[DependencyResolver] Use merging for constraint container update.
- This fixes some subtlety in the previous code about which merge operations could perform partial update on the container, some call sites relied critically on one container not doing that. - Instead, switch both containers to only expose a `merging` method which provides a new result. This is somewhat less efficient with the current implementations, but should go out in the wash once we switch to more efficient implementations backed by persistent data structures.
1 parent 29ea639 commit a9ed567

File tree

2 files changed

+75
-65
lines changed

2 files changed

+75
-65
lines changed

Sources/PackageGraph/DependencyResolver.swift

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ func ==(_ lhs: BoundVersion, _ rhs: BoundVersion) -> Bool {
208208
}
209209

210210
/// A container for constraints for a set of packages.
211+
///
212+
/// This data structure is only designed to represent satisfiable constraint
213+
/// sets, it cannot represent sets including containers which have an empty
214+
/// constraint.
211215
//
212216
// FIXME: Maybe each package should just return this, instead of a list of
213217
// `PackageContainerConstraint`s. That won't work if we decide this should
@@ -229,7 +233,10 @@ struct PackageContainerConstraintSet<C: PackageContainer>: Collection {
229233
}
230234

231235
/// Create an constraint set from known values.
236+
///
237+
/// The initial constraints should never be unsatisfiable.
232238
init(_ constraints: [Identifier: VersionSetSpecifier]) {
239+
assert(constraints.values.filter({ $0 == .empty }).isEmpty)
233240
self.constraints = constraints
234241
}
235242

@@ -243,41 +250,45 @@ struct PackageContainerConstraintSet<C: PackageContainer>: Collection {
243250
return constraints[identifier] ?? .any
244251
}
245252

246-
/// Merge the given version requirement for the container `identifier`.
253+
/// Create a constraint set by merging the `versionRequirement` for container `identifier`.
247254
///
248-
/// - Returns: False if the merger has made the set unsatisfiable; i.e. true
249-
/// when the resulting set is satisfiable, if it was already so.
250-
private mutating func merge(versionRequirement: VersionSetSpecifier, for identifier: Identifier) -> Bool {
251-
let intersection: VersionSetSpecifier
252-
if let existing = constraints[identifier] {
253-
intersection = existing.intersection(versionRequirement)
254-
} else {
255-
intersection = versionRequirement
255+
/// - Returns: The new set, or nil the resulting set is unsatisfiable.
256+
private func merging(
257+
versionRequirement: VersionSetSpecifier, for identifier: Identifier
258+
) -> PackageContainerConstraintSet<C>?
259+
{
260+
let intersection = self[identifier].intersection(versionRequirement)
261+
if intersection == .empty {
262+
return nil
256263
}
257-
constraints[identifier] = intersection
258-
return intersection != .empty
264+
var result = self
265+
result.constraints[identifier] = intersection
266+
return result
259267
}
260268

261-
/// Merge the given `constraint`.
269+
/// Create a constraint set by merging `constraint`.
262270
///
263-
/// - Returns: False if the merger has made the set unsatisfiable; i.e. true
264-
/// when the resulting set is satisfiable, if it was already so.
265-
mutating func merge(_ constraint: PackageContainerConstraint<Identifier>) -> Bool {
266-
return merge(versionRequirement: constraint.versionRequirement, for: constraint.identifier)
271+
/// - Returns: The new set, or nil the resulting set is unsatisfiable.
272+
func merging(_ constraint: PackageContainerConstraint<Identifier>) -> PackageContainerConstraintSet<C>? {
273+
return merging(versionRequirement: constraint.versionRequirement, for: constraint.identifier)
267274
}
268275

269-
/// Merge the given constraint set.
276+
/// Create a new constraint set by merging the given constraint set.
270277
///
271278
/// - Returns: False if the merger has made the set unsatisfiable; i.e. true
272279
/// when the resulting set is satisfiable, if it was already so.
273-
mutating func merge(_ constraints: PackageContainerConstraintSet<Container>) -> Bool {
274-
var satisfiable = true
280+
func merging(
281+
_ constraints: PackageContainerConstraintSet<Container>
282+
) -> PackageContainerConstraintSet<C>?
283+
{
284+
var result = self
275285
for (key, versionRequirement) in constraints {
276-
if !merge(versionRequirement: versionRequirement, for: key) {
277-
satisfiable = false
286+
guard let merged = result.merging(versionRequirement: versionRequirement, for: key) else {
287+
return nil
278288
}
289+
result = merged
279290
}
280-
return satisfiable
291+
return result
281292
}
282293

283294
// MARK: Collection Conformance
@@ -343,41 +354,36 @@ struct VersionAssignmentSet<C: PackageContainer>: Sequence {
343354
}
344355
}
345356

346-
/// Merge in the bindings from the given `assignment`.
357+
/// Create a new assignment set by merging in the bindings from `assignment`.
347358
///
348-
/// - Returns: False if the merge cannot be made (the assignments contain
349-
/// incompatible versions).
350-
mutating func merge(_ assignment: VersionAssignmentSet<Container>) -> Bool {
359+
/// - Returns: The new assignment, or nil if the merge cannot be made (the
360+
/// assignments contain incompatible versions).
361+
func merging(_ assignment: VersionAssignmentSet<Container>) -> VersionAssignmentSet<Container>? {
351362
// In order to protect the assignment set, we first have to test whether
352363
// the merged constraint sets are satisfiable.
353364
//
354-
// FIXME: Move to non-mutating methods with results, in order to have a
355-
// nice consistent API with `PackageContainerConstraintSet.merge`.
356-
//
357365
// FIXME: This is very inefficient; we should decide whether it is right
358366
// to handle it here or force the main resolver loop to handle the
359367
// discovery of this property.
360-
var mergedConstraints = constraints
361-
guard mergedConstraints.merge(assignment.constraints) else {
362-
return false
368+
guard let _ = constraints.merging(assignment.constraints) else {
369+
return nil
363370
}
364371

365372
// The induced constraints are satisfiable, so we *can* union the
366373
// assignments without breaking our internal invariant on
367374
// satisfiability.
375+
var result = self
368376
for (container, binding) in assignment {
369-
if let existing = self[container] {
377+
if let existing = result[container] {
370378
if existing != binding {
371-
// NOTE: We are returning here with the data structure
372-
// partially updated, which feels wrong. See FIXME above.
373-
return false
379+
return nil
374380
}
375381
} else {
376-
self[container] = binding
382+
result[container] = binding
377383
}
378384
}
379385

380-
return true
386+
return result
381387
}
382388

383389
/// The combined version constraints induced by the assignment.
@@ -407,8 +413,10 @@ struct VersionAssignmentSet<C: PackageContainer>: Sequence {
407413
// FIXME: Error handling, except that we probably shouldn't have
408414
// needed to refetch the dependencies at this point.
409415
for constraint in try! container.getDependencies(at: version) {
410-
let satisfiable = result.merge(constraint)
411-
assert(satisfiable)
416+
guard let merged = result.merging(constraint) else {
417+
preconditionFailure("unsatisfiable constraint set")
418+
}
419+
result = merged
412420
}
413421
}
414422
}
@@ -661,9 +669,10 @@ public class DependencyResolver<
661669
// FIXME: We should have a test for this, probably by adding some kind
662670
// of statistics on the number of backtracks.
663671
for constraint in constraints {
664-
if !allConstraints.merge(constraint) {
672+
guard let merged = allConstraints.merging(constraint) else {
665673
return nil
666674
}
675+
allConstraints = merged
667676
}
668677

669678
for constraint in constraints {
@@ -685,12 +694,9 @@ public class DependencyResolver<
685694
throw DependencyResolverError.unimplemented
686695
}
687696

688-
// We found a valid assignment, attempt to merge it with the current solution.
689-
//
690-
// FIXME: It is rather important, subtle, and confusing that this
691-
// `merge` doesn't mutate the assignment but the one on the
692-
// constraint set does. We should probably make them consistent.
693-
guard assignment.merge(subtreeAssignment) else {
697+
// We found a valid subtree assignment, attempt to merge it with the
698+
// current solution.
699+
guard let newAssignment = assignment.merging(subtreeAssignment) else {
694700
// The assignment couldn't be merged with the current
695701
// assignment, or the constraint sets couldn't be merged.
696702
//
@@ -701,13 +707,16 @@ public class DependencyResolver<
701707
throw DependencyResolverError.unimplemented
702708
}
703709

704-
// Merge the working constraint set.
710+
// Update the working assignment and constraint set.
705711
//
706712
// This should always be feasible, because all prior constraints
707713
// were part of the input constraint request (see comment around
708714
// initial `merge` outside the loop).
709-
let mergable = allConstraints.merge(subtreeAssignment.constraints)
710-
precondition(mergable)
715+
assignment = newAssignment
716+
guard let merged = allConstraints.merging(subtreeAssignment.constraints) else {
717+
preconditionFailure("unsatisfiable constraints while merging subtree")
718+
}
719+
allConstraints = merged
711720
}
712721

713722
return assignment

Tests/PackageGraphTests/DependencyResolverTests.swift

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -141,27 +141,25 @@ class DependencyResolverTests: XCTestCase {
141141
XCTAssertEqual(set.containerIdentifiers.map{ $0 }, [])
142142

143143
// Check basics.
144-
XCTAssertTrue(set.merge(MockPackageConstraint(container: "A", versionRequirement: v1Range)))
144+
set = set.merging(MockPackageConstraint(container: "A", versionRequirement: v1Range))!
145145
XCTAssertEqual(set.containerIdentifiers.map{ $0 }, ["A"])
146146
XCTAssertEqual(set["A"], v1Range)
147-
XCTAssertTrue(set.merge(MockPackageConstraint(container: "B", versionRequirement: v2Range)))
147+
set = set.merging(MockPackageConstraint(container: "B", versionRequirement: v2Range))!
148148
XCTAssertEqual(set.containerIdentifiers.sorted(), ["A", "B"])
149149

150150
// Check merging a constraint which makes the set unsatisfiable.
151-
XCTAssertFalse(set.merge(MockPackageConstraint(container: "A", versionRequirement: v2Range)))
152-
XCTAssertEqual(set["A"], VersionSetSpecifier.empty)
151+
XCTAssert(set.merging(MockPackageConstraint(container: "A", versionRequirement: v2Range)) == nil)
153152

154153
// Check merging other sets.
155154
var set2 = ConstraintSet()
156-
_ = set2.merge(MockPackageConstraint(container: "C", versionRequirement: v1Range))
157-
XCTAssertTrue(set.merge(set2))
155+
set2 = set2.merging(MockPackageConstraint(container: "C", versionRequirement: v1Range))!
156+
set = set.merging(set2)!
158157
XCTAssertEqual(set.containerIdentifiers.map{ $0 }.sorted(), ["A", "B", "C"])
159158
var set3 = ConstraintSet()
160-
_ = set3.merge(MockPackageConstraint(container: "C", versionRequirement: v2Range))
161-
_ = set3.merge(MockPackageConstraint(container: "D", versionRequirement: v1Range))
162-
_ = set3.merge(MockPackageConstraint(container: "E", versionRequirement: v1Range))
163-
XCTAssertFalse(set.merge(set3)) // "C" requirement is unsatisfiable
164-
XCTAssertEqual(set.containerIdentifiers.map{ $0 }.sorted(), ["A", "B", "C", "D", "E"])
159+
set3 = set3.merging(MockPackageConstraint(container: "C", versionRequirement: v2Range))!
160+
set3 = set3.merging(MockPackageConstraint(container: "D", versionRequirement: v1Range))!
161+
set3 = set3.merging(MockPackageConstraint(container: "E", versionRequirement: v1Range))!
162+
XCTAssert(set.merging(set3) == nil) // "C" requirement is unsatisfiable
165163
}
166164

167165
func testVersionAssignment() {
@@ -214,21 +212,24 @@ class DependencyResolverTests: XCTestCase {
214212
v2: []])
215213
var assignment2 = VersionAssignmentSet<MockPackageContainer>()
216214
assignment2[d] = .version(v1)
217-
XCTAssertTrue(assignment.merge(assignment2))
215+
if let mergedAssignment = assignment.merging(assignment2) {
216+
assignment = mergedAssignment
217+
} else {
218+
return XCTFail("unexpected failure merging assignment")
219+
}
218220
XCTAssertEqual(assignment.constraints, ["C": v1_0Range, "E": v1Range])
219221

220222
// Check merger of an assignment with incompatible constraints.
221223
let d2 = MockPackageContainer(name: "D2", dependenciesByVersion: [
222224
v1: [(container: "E", versionRequirement: v2Range)]])
223225
var assignment3 = VersionAssignmentSet<MockPackageContainer>()
224226
assignment3[d2] = .version(v1)
225-
XCTAssertFalse(assignment.merge(assignment3))
226-
XCTAssertEqual(assignment.constraints, ["C": v1_0Range, "E": v1Range])
227+
XCTAssertEqual(assignment.merging(assignment3), nil)
227228

228229
// Check merger of an incompatible assignment.
229230
var assignment4 = VersionAssignmentSet<MockPackageContainer>()
230231
assignment4[d] = .version(v2)
231-
XCTAssertFalse(assignment.merge(assignment4))
232+
XCTAssertEqual(assignment.merging(assignment4), nil)
232233
}
233234

234235
/// Check the basic situations for resolving a subtree.

0 commit comments

Comments
 (0)