Skip to content

Commit dbcc432

Browse files
committed
Try to optimize __ExpectationContext a bit
1 parent c9d3cec commit dbcc432

File tree

2 files changed

+57
-29
lines changed

2 files changed

+57
-29
lines changed

Sources/Testing/Expectations/ExpectationChecking+Macro.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ public func __checkClosureCall<R>(
511511
mismatchExplanationValue = explanation
512512
} catch {
513513
caughtError = error
514-
expectationContext.runtimeValues[.root] = { Expression.Value(reflecting: error) }
514+
expectationContext.captureValue(error, identifiedBy: .root)
515515
let secondError = Issue.withErrorRecording(at: sourceLocation) {
516516
errorMatches = try errorMatcher(error)
517517
}
@@ -562,7 +562,7 @@ public nonisolated(nonsending) func __checkClosureCall<R>(
562562
mismatchExplanationValue = explanation
563563
} catch {
564564
caughtError = error
565-
expectationContext.runtimeValues[.root] = { Expression.Value(reflecting: error) }
565+
expectationContext.captureValue(error, identifiedBy: .root)
566566
let secondError = await Issue.withErrorRecording(at: sourceLocation) {
567567
errorMatches = try await errorMatcher(error)
568568
}

Sources/Testing/Expectations/ExpectationContext.swift

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,28 @@ public final class __ExpectationContext<Output> where Output: ~Copyable {
3939
/// will have runtime values: notably, if an operand to a short-circuiting
4040
/// binary operator like `&&` is not evaluated, the corresponding expression
4141
/// will not be assigned a runtime value.
42-
var runtimeValues: [__ExpressionID: () -> Expression.Value?]
42+
///
43+
/// This property is non-optional because the evaluation of an expectation
44+
/// always produces at least one element.
45+
var runtimeValues: [(__ExpressionID, () -> Expression.Value?)]
4346

4447
/// Computed differences between the operands or arguments of expressions.
4548
///
4649
/// The values in this dictionary are gathered at runtime as subexpressions
4750
/// are evaluated, much like ``runtimeValues``.
48-
var differences: [__ExpressionID: () -> CollectionDifference<Any>?]
51+
///
52+
/// This value is optional because, in the common case, an expectation does
53+
/// not produce a difference.
54+
var differences: [(__ExpressionID, () -> CollectionDifference<Any>?)]?
4955

5056
init(
51-
sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String> = [:],
52-
runtimeValues: [__ExpressionID: () -> Expression.Value?] = [:],
53-
differences: [__ExpressionID: () -> CollectionDifference<Any>?] = [:]
57+
sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>,
58+
runtimeValues: KeyValuePairs<__ExpressionID, () -> Expression.Value?>? = nil,
59+
differences: KeyValuePairs<__ExpressionID, () -> CollectionDifference<Any>?>? = nil
5460
) {
5561
_sourceCode = sourceCode
56-
self.runtimeValues = runtimeValues
57-
self.differences = differences
62+
self.runtimeValues = runtimeValues.map(Array.init) ?? []
63+
self.differences = differences.map(Array.init)
5864
}
5965

6066
/// Collapse the given expression graph into one or more expressions with
@@ -122,12 +128,14 @@ public final class __ExpectationContext<Output> where Output: ~Copyable {
122128
}
123129
}
124130

125-
for (id, difference) in differences {
126-
let keyPath = id.keyPathRepresentation
127-
if var expression = expressionGraph[keyPath], let difference = difference() {
128-
let differenceDescription = Self._description(of: difference)
129-
expression.differenceDescription = differenceDescription
130-
expressionGraph[keyPath] = expression
131+
if let differences {
132+
for (id, difference) in differences {
133+
let keyPath = id.keyPathRepresentation
134+
if var expression = expressionGraph[keyPath], let difference = difference() {
135+
let differenceDescription = Self._description(of: difference)
136+
expression.differenceDescription = differenceDescription
137+
expressionGraph[keyPath] = expression
138+
}
131139
}
132140
}
133141
}
@@ -159,6 +167,19 @@ extension __ExpectationContext: Sendable where Output: ~Copyable {}
159167
// MARK: - Expression capturing
160168

161169
extension __ExpectationContext where Output: ~Copyable {
170+
/// Capture information about a value, encapsulated in an instance of
171+
/// ``Expression/Value``, for use if the expectation currently being evaluated
172+
/// fails.
173+
///
174+
/// - Parameters:
175+
/// - runtimeValue: The value to pass through. This value is lazily
176+
/// evaluated on expectation failure only.
177+
/// - id: A value that uniquely identifies the represented expression in the
178+
/// context of the expectation currently being evaluated.
179+
func captureValue(_ runtimeValue: @escaping @autoclosure () -> Expression.Value?, identifiedBy id: __ExpressionID) {
180+
runtimeValues.append((id, runtimeValue))
181+
}
182+
162183
/// Capture information about a value for use if the expectation currently
163184
/// being evaluated fails.
164185
///
@@ -167,15 +188,13 @@ extension __ExpectationContext where Output: ~Copyable {
167188
/// - id: A value that uniquely identifies the represented expression in the
168189
/// context of the expectation currently being evaluated.
169190
///
170-
/// - Returns: `value`, verbatim.
171-
///
172191
/// This function helps subscript overloads disambiguate themselves and avoid
173192
/// accidental recursion.
174-
func captureValue<T>(_ value: borrowing T, _ id: __ExpressionID) where T: ~Copyable & ~Escapable {
193+
func captureValue<T>(_ value: borrowing T, identifiedBy id: __ExpressionID) where T: ~Copyable & ~Escapable {
175194
if #available(_castingWithNonCopyableGenerics, *), let value = makeExistential(value) {
176-
runtimeValues[id] = { Expression.Value(reflecting: value) }
195+
captureValue(Expression.Value(reflecting: value), identifiedBy: id)
177196
} else {
178-
runtimeValues[id] = { Expression.Value(failingToReflectInstanceOf: T.self) }
197+
captureValue(Expression.Value(failingToReflectInstanceOf: T.self), identifiedBy: id)
179198
}
180199
}
181200

@@ -193,7 +212,7 @@ extension __ExpectationContext where Output: ~Copyable {
193212
/// `#require()` macros. Do not call it directly.
194213
@_lifetime(borrow value)
195214
public func callAsFunction<T>(_ value: borrowing T, _ id: __ExpressionID) -> T where T: ~Escapable {
196-
captureValue(value, id)
215+
captureValue(value, identifiedBy: id)
197216
return copy value
198217
}
199218

@@ -208,7 +227,7 @@ extension __ExpectationContext where Output: ~Copyable {
208227
/// - Warning: This function is used to implement the `#expect()` and
209228
/// `#require()` macros. Do not call it directly.
210229
public func __inoutAfter<T>(_ value: inout T, _ id: __ExpressionID) where T: ~Copyable & ~Escapable {
211-
captureValue(value, id)
230+
captureValue(value, identifiedBy: id)
212231
}
213232
}
214233

@@ -253,12 +272,13 @@ extension __ExpectationContext where Output: ~Copyable {
253272
/// compile-time pressure on the type checker that we don't want.
254273
func captureDifferences<T, U>(_ lhs: T, _ rhs: U, _ opID: __ExpressionID) {
255274
#if !hasFeature(Embedded) // no existentials
275+
var difference: (() -> CollectionDifference<Any>?)?
256276
if let lhs = lhs as? any StringProtocol {
257277
func open<V>(_ lhs: V, _ rhs: U) where V: StringProtocol {
258278
guard let rhs = rhs as? V else {
259279
return
260280
}
261-
differences[opID] = {
281+
difference = {
262282
// Compare strings by line, not by character.
263283
let lhsLines = String(lhs).split(whereSeparator: \.isNewline)
264284
let rhsLines = String(rhs).split(whereSeparator: \.isNewline)
@@ -290,7 +310,7 @@ extension __ExpectationContext where Output: ~Copyable {
290310
let elementType = V.Element.self as? any Equatable.Type else {
291311
return
292312
}
293-
differences[opID] = {
313+
difference = {
294314
func open<E>(_: E.Type) -> CollectionDifference<Any> where E: Equatable {
295315
let lhs: some BidirectionalCollection<E> = lhs.lazy.map { $0 as! E }
296316
let rhs: some BidirectionalCollection<E> = rhs.lazy.map { $0 as! E }
@@ -301,6 +321,14 @@ extension __ExpectationContext where Output: ~Copyable {
301321
}
302322
open(lhs, rhs)
303323
}
324+
325+
if let difference {
326+
if differences == nil {
327+
differences = [(opID, difference)]
328+
} else {
329+
differences?.append((opID, difference))
330+
}
331+
}
304332
#endif
305333
}
306334

@@ -329,11 +357,11 @@ extension __ExpectationContext where Output: ~Copyable {
329357
_ rhs: borrowing U,
330358
_ rhsID: __ExpressionID
331359
) throws(E) -> Bool where T: ~Copyable, U: ~Copyable {
332-
captureValue(lhs, lhsID)
333-
captureValue(rhs, rhsID)
360+
captureValue(lhs, identifiedBy: lhsID)
361+
captureValue(rhs, identifiedBy: rhsID)
334362

335363
let result = try op(lhs, rhs)
336-
captureValue(result, opID)
364+
captureValue(result, identifiedBy: opID)
337365

338366
if !result,
339367
#available(_castingWithNonCopyableGenerics, *),
@@ -370,12 +398,12 @@ extension __ExpectationContext where Output: ~Copyable {
370398
public func __as<T, U>(_ value: borrowing T, _ valueID: __ExpressionID, _ type: U.Type, _ typeID: __ExpressionID) -> U? {
371399
let value = copy value
372400

373-
captureValue(value, valueID)
401+
captureValue(value, identifiedBy: valueID)
374402
let result = value as? U
375403

376404
if result == nil {
377405
let correctType = Swift.type(of: value as Any)
378-
captureValue(correctType, typeID)
406+
captureValue(correctType, identifiedBy: typeID)
379407
}
380408

381409
return result

0 commit comments

Comments
 (0)