Skip to content

Commit c5c27c7

Browse files
committed
[stdlib] Add a missing debug precondition to [Closed]Range.init(uncheckedBounds:)
Unchecked APIs must still perform checks in debug builds to ensure that invariants aren’t violated. (I.e., these aren’t a license to perform invalid operations — they just let us get rid of the checks when we know for sure they are unnecessary.)
1 parent 7f63b42 commit c5c27c7

File tree

4 files changed

+55
-9
lines changed

4 files changed

+55
-9
lines changed

stdlib/public/core/ClosedRange.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ public struct ClosedRange<Bound: Comparable> {
6868
/// The range's upper bound.
6969
public let upperBound: Bound
7070

71+
@_alwaysEmitIntoClient @inline(__always)
72+
internal init(_uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
73+
self.lowerBound = bounds.lower
74+
self.upperBound = bounds.upper
75+
}
76+
7177
/// Creates an instance with the given bounds.
7278
///
7379
/// Because this initializer does not perform any checks, it should be used
@@ -78,8 +84,9 @@ public struct ClosedRange<Bound: Comparable> {
7884
/// - Parameter bounds: A tuple of the lower and upper bounds of the range.
7985
@inlinable
8086
public init(uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
81-
self.lowerBound = bounds.lower
82-
self.upperBound = bounds.upper
87+
_debugPrecondition(bounds.lower <= bounds.upper,
88+
"ClosedRange requires lowerBound <= upperBound")
89+
self.init(_uncheckedBounds: (lower: bounds.lower, upper: bounds.upper))
8390
}
8491
}
8592

@@ -336,7 +343,7 @@ extension Comparable {
336343
public static func ... (minimum: Self, maximum: Self) -> ClosedRange<Self> {
337344
_precondition(
338345
minimum <= maximum, "Range requires lowerBound <= upperBound")
339-
return ClosedRange(uncheckedBounds: (lower: minimum, upper: maximum))
346+
return ClosedRange(_uncheckedBounds: (lower: minimum, upper: maximum))
340347
}
341348
}
342349

stdlib/public/core/Range.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ public struct Range<Bound: Comparable> {
157157
/// instance does not contain its upper bound.
158158
public let upperBound: Bound
159159

160+
@_alwaysEmitIntoClient @inline(__always)
161+
internal init(_uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
162+
self.lowerBound = bounds.lower
163+
self.upperBound = bounds.upper
164+
}
165+
160166
/// Creates an instance with the given bounds.
161167
///
162168
/// Because this initializer does not perform any checks, it should be used
@@ -167,8 +173,9 @@ public struct Range<Bound: Comparable> {
167173
/// - Parameter bounds: A tuple of the lower and upper bounds of the range.
168174
@inlinable
169175
public init(uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
170-
self.lowerBound = bounds.lower
171-
self.upperBound = bounds.upper
176+
_debugPrecondition(bounds.lower <= bounds.upper,
177+
"Range requires lowerBound <= upperBound")
178+
self.init(_uncheckedBounds: (lower: bounds.lower, upper: bounds.upper))
172179
}
173180

174181
/// Returns a Boolean value indicating whether the given element is contained
@@ -729,7 +736,7 @@ extension Comparable {
729736
public static func ..< (minimum: Self, maximum: Self) -> Range<Self> {
730737
_precondition(minimum <= maximum,
731738
"Range requires lowerBound <= upperBound")
732-
return Range(uncheckedBounds: (lower: minimum, upper: maximum))
739+
return Range(_uncheckedBounds: (lower: minimum, upper: maximum))
733740
}
734741

735742
/// Returns a partial range up to, but not including, its upper bound.

test/stdlib/RangeTraps.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,5 +116,23 @@ RangeTraps.test("throughNaN")
116116
_ = ...Double.nan
117117
}
118118

119+
RangeTraps.test("UncheckedHalfOpen")
120+
.xfail(.custom(
121+
{ !_isDebugAssertConfiguration() },
122+
reason: "assertions are disabled in Release and Unchecked mode"))
123+
.code {
124+
expectCrashLater()
125+
var range = Range(uncheckedBounds: (lower: 1, upper: 0))
126+
}
127+
128+
RangeTraps.test("UncheckedClosed")
129+
.xfail(.custom(
130+
{ !_isDebugAssertConfiguration() },
131+
reason: "assertions are disabled in Release and Unchecked mode"))
132+
.code {
133+
expectCrashLater()
134+
var range = ClosedRange(uncheckedBounds: (lower: 1, upper: 0))
135+
}
136+
119137
runAllTests()
120138

validation-test/stdlib/Range.swift.gyb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,17 +335,31 @@ extension ${Self} where Bound : TestProtocol1 {
335335
var ${TestSuite} = TestSuite("${Self}")
336336

337337
${TestSuite}.test("init(uncheckedBounds:)")
338-
.forEach(in: [(1, 2), (1, 1), (2, 1)]) {
338+
.forEach(in: [(1, 2), (1, 1)]) {
339339
(lowerInt, upperInt) in
340340

341-
// Check that 'init(uncheckedBounds:)' does not perform precondition checks,
342-
// allowing to create ranges that break invariants.
343341
let r = ${Self}(
344342
uncheckedBounds: (lower: ${Bound}(lowerInt), upper: ${Bound}(upperInt)))
345343
expectEqual(lowerInt, r.lowerBound.value)
346344
expectEqual(upperInt, r.upperBound.value)
347345
}
348346

347+
${TestSuite}.test("init(uncheckedBounds:) with invalid values")
348+
.xfail(
349+
.custom(
350+
{ _isDebugAssertConfiguration() },
351+
reason: "unchecked initializer still checks its input in Debug mode"))
352+
.code {
353+
let low = 2
354+
let up = 1
355+
// Check that 'init(uncheckedBounds:)' does not perform precondition checks,
356+
// allowing to create ranges that break invariants.
357+
let r = ${Self}(
358+
uncheckedBounds: (lower: ${Bound}(low), upper: ${Bound}(up)))
359+
expectEqual(low, r.lowerBound.value)
360+
expectEqual(up, r.upperBound.value)
361+
}
362+
349363
% for (DestinationSelf, _, _, _) in all_range_types:
350364
${TestSuite}.test("init(${DestinationSelf})/whereBoundIsStrideable")
351365
.forEach(in: [(0, 0), (1, 2), (10, 20), (Int.min, Int.max)]) {

0 commit comments

Comments
 (0)