Skip to content

Commit 75dd206

Browse files
committed
types: fix bug in "halving" variant of the union-bound algorithm
In the union-bound algorithm we track our unified type variables as sets, which are represented by trees of pointers, where the roots of the trees are used as set representatives for determining equality etc. When unifying variables, we move one's tree into the tree of the other, and then copy all its bounds to the root of the new unified tree. Most operations within union-bound are given an arbitrary set element and immediately locate its root before doing anything. We obtain better asymptotics by using the "halving" variant of union-bound, wherein on each root lookup, as we follow the path from the element to its root, we update the links to skip every other one. Basically, if node x points to its parent which points to a grandparent, we update x to point to the grandparent directly. *However* our existing code, rather than making x point to the grandparent, would just copy the grandparent over the parent. I'm not sure what I was thinking here -- I think I got tangled up trying to deal with Rust's multiple-aliasing rules without deadlocking and lost track of the actual algorithm, or maybe I misread Russell's C code which I was mostly copying from -- but the result is both weird and incorrect. In particular, if the grandparent is a root, then by copying its data into parent, we duplicate the root, effectively un-unifying the variable at some hard-to-predict point. Once the un-unification happens, then future unifications will appear to only get half-applied, the resulting type inference will be slightly wrong, we will produce programs which fail type-checking or sharing-checking after being encoded, even though they were successfully constructed. The fix, amusingly, actually reduces the amount of locking, which might provide a slight speedup, and will certainly make it easier to convince yourself that this code is deadlock-free. (Probably we should drop all these mutexes and just use raw pointers and unsafe code, which would be easier to follow; the mutexes serve no purpose other than to mollify the Rust typechecker; we assure type-safety by locking the entire slab, and locking individual set elements is neither necessary nor sufficient to get a thread-safe type inference algorithm.) Thanks to Russell O'Connor for identifying this bug, and some thanks to Claude 3.7-sonnet for producing weird not-quite-sensible "fixes" that would change observed behavior on our bad test cases, and pointed us to roughly the correct location.
1 parent 78288bc commit 75dd206

File tree

1 file changed

+6
-17
lines changed

1 file changed

+6
-17
lines changed

src/types/union_bound.rs

+6-17
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,6 @@ impl<T> UbData<T> {
9393
}
9494
}
9595

96-
impl<T: PointerLike> UbData<T> {
97-
fn shallow_clone(&self) -> Self {
98-
match self {
99-
UbData::Root(x) => UbData::Root(x.shallow_clone()),
100-
UbData::EqualTo(eq) => UbData::EqualTo(eq.shallow_clone()),
101-
}
102-
}
103-
}
104-
10596
impl<T> UbElement<T> {
10697
/// Turns an existing piece of data into a singleton union-bound set.
10798
pub fn new(data: T) -> Self {
@@ -148,21 +139,19 @@ impl<T: PointerLike> UbElement<T> {
148139
}
149140
};
150141

151-
let mut parent_inner_lock = parent.inner.lock().unwrap();
142+
let parent_inner_lock = parent.inner.lock().unwrap();
152143
// If the parent has a parent, remove the intermediate link. (This is
153144
// the "halving" variant of union-bound.)
154145
match parent_inner_lock.data {
155146
// If there is no grandparent, return the parent.
156147
UbData::Root(..) => return parent.shallow_clone(),
157148
UbData::EqualTo(ref grandparent) => {
158149
let grandparent = grandparent.shallow_clone();
159-
// Okay to lock both grandparent and parent at once because the
160-
// algorithm guarantees there are no cycles.
161-
let gp_inner_lock = grandparent.inner.lock().unwrap();
162-
parent_inner_lock.data = gp_inner_lock.data.shallow_clone();
163-
drop(gp_inner_lock);
164-
drop(parent_inner_lock);
165-
x = grandparent.shallow_clone();
150+
// The previous `x_inner_lock` was dropped above, so this will not deadlock.
151+
let mut x_inner_lock = x.inner.lock().unwrap();
152+
x_inner_lock.data = UbData::EqualTo(grandparent.shallow_clone());
153+
drop(x_inner_lock);
154+
x = grandparent;
166155
}
167156
}
168157
}

0 commit comments

Comments
 (0)