Yep, and the reason is that there are some limitations with infinitely-recursive associated types. You can have an associated type that recursively conforms to a single protocol, generating infinitely many nested types:
protocol P {
associatedtype A: P
}
The above is the SwiftUI View protocol essentially.
You can also inherit from P in a new protocol Q and impose an additional conformance requirement on A, which is then applied to this infinite set of type parameters:
protocol Q: P where A: Q {
// (that re-stating an associated type has no effect on generic signatures)
// associatedtype A: ...
}
This works because protocol Q is "more specific" than P (technically, it precedes P in the reduction order because it inherits from P). The above pattern comes up in the standard library Collection protocol hierarchy.
You can do more complicated things if the recursion ends after a finite number of steps with a same-type requirement:
protocol P {
associatedtype A: P
}
protocol Q {
associatedtype A: P & Q where Self == A.A.A.A
}
If the set of type parameters is finite then we can always build a rewrite system in a finite number of steps (it's still undecidable if a rewrite system has finitely many irreducible terms, so technically the completion procedure might hit a cutoff before discovering all the rules, but it's unlikely that any reasonable protocol hierarchy will impose requirements on unique type parameters of length 10. The limit can be increased with a frontend flag.)
What doesn't always work is a recursive associated type with arbitrary conformance requirements involving unrelated protocols -- the below is the same as the previous example, just without Self == A.A.A.A:
protocol P {
associatedtype A: P
}
protocol Q {
associatedtype A: P & Q
}
error: cannot build rewrite system for protocol; rule length limit exceeded
protocol Q {
^
note: failed rewrite rule is [Q:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[Q] => [Q:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A].[P:A]
protocol Q {
^
Here, P precedes Q in the reduction order because both inherit from the same number of protocols (this concept will be important shortly) and P < Q in lexicographic order on identifiers. Completion then blows up with an error because a convergent presentation of this protocol needs infinitely many conformance requirements.
If you rename protocol Q to protocol M or something, then it works! Now, M < P, and we get away with just adding finitely many additional rules.
Also, if protocol Q were to inherit from more protocols than P (if you consider the transitive closure of the inheritance graph) then it also works. And note, it doesn't have to inherit from P itself, just from more protocols than P! (For if A inherits from B, then it is also true that A inherits from more protocols than B, but the converse isn't always true).
protocol Other {}
protocol P {
associatedtype A: P
}
protocol Q: Other {
associatedtype A: P & Q
}
And it is this latter case that is triggered by FixedWidthInteger. The inherited Magnitude and Stride associated types receive a rather complex set of requirements, and they are themselves recursive in the base protocol:
public protocol FixedWidthInteger: BinaryInteger, LosslessStringConvertible
where Magnitude: FixedWidthInteger & UnsignedInteger,
Stride: FixedWidthInteger & SignedInteger {
It works in the stdlib as written because FixedWidthInteger precedes BinaryInteger, SignedInteger and UnsignedInteger in the reduction order. The reduction order is designed to ensure that FixedWidthInteger < BinaryInteger but the other too were only true by luck. In your case, your luck ran out and the relationship changed so that FixedWidthInteger succeeded SignedInteger, because you removed the LosslessStringConvertible protocol.
You can make a good case that independent of implementation limitation, FixedWidthInteger should probably have also had same-type requirements like Stride.Stride == Stride and Magnitude.Magnitude == Magnitude from the start, since in practice those relations should hold. Adding those requirements would avoid the possibility of this issue completely. But technically it would be an ABI break if someone had a conforming type which did not satisfy them.
Interestingly, Rust cannot even represent the "recursive conformance imposed on inherited associated type" case that comes up in Swift with, eg, BidirectionalCollection and RandomAccessCollection, which require that the Indices and SubSequence associated types conform to the same protocol. However, they have generic associated types, which are even more difficult to reason about for the type checker.