Skip to content

Support property accesses on ~Escapable types #1154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ let package = Package(
],
exclude: ["CMakeLists.txt", "Testing.swiftcrossimport"],
cxxSettings: .packageSettings,
swiftSettings: .packageSettings + .enableLibraryEvolution(),
swiftSettings: .packageSettings + .enableLibraryEvolution()
+ [.enableExperimentalFeature("LifetimeDependence")],
linkerSettings: [
.linkedLibrary("execinfo", .when(platforms: [.custom("freebsd"), .openbsd]))
]
Expand All @@ -130,6 +131,7 @@ let package = Package(
"MemorySafeTestingTests",
],
swiftSettings: .packageSettings + .disableMandatoryOptimizationsSettings
+ [.enableExperimentalFeature("LifetimeDependence")]
),

// Use a plain `.target` instead of a `.testTarget` to avoid the unnecessary
Expand Down
4 changes: 2 additions & 2 deletions Sources/Testing/Expectations/Expectation+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
/// If `optionalValue` is `nil`, an ``Issue`` is recorded for the test that is
/// running in the current task and an instance of ``ExpectationFailedError`` is
/// thrown.
@freestanding(expression) public macro require<T>(
@freestanding(expression) public macro require<T: ~Escapable>(
_ optionalValue: T?,
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation
Expand Down Expand Up @@ -123,7 +123,7 @@ public macro require(
/// diagnostic indicating that the expectation is redundant.
@freestanding(expression)
@_documentation(visibility: private)
public macro require<T>(
public macro require<T: ~Escapable>(
_ optionalValue: T,
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation
Expand Down
103 changes: 103 additions & 0 deletions Sources/Testing/Expectations/ExpectationChecking+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,36 @@ public func __checkPropertyAccess<T>(
)
}

/// Check that an expectation has passed after a condition has been evaluated
/// and throw an error if it failed, for nonescapable types.
///
/// This overload is used by property accesses:
///
/// ```swift
/// #expect(x.isFoodTruck)
/// ```
///
/// - Warning: This function is used to implement the `#expect()` and
/// `#require()` macros. Do not call it directly.
public func __checkPropertyAccess<T: ~Escapable>(
_ lhs: T, getting memberAccess: (T) -> Bool,
expression: __Expression,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
sourceLocation: SourceLocation
) -> Result<Void, any Error> {
let condition = memberAccess(lhs)
return __checkValue(
condition,
expression: expression,
expressionWithCapturedRuntimeValues:
expression.capturingRuntimeValues("<nonescapable>", condition),
comments: comments(),
isRequired: isRequired,
sourceLocation: sourceLocation
)
}

/// Check that an expectation has passed after a condition has been evaluated
/// and throw an error if it failed.
///
Expand Down Expand Up @@ -586,6 +616,39 @@ public func __checkPropertyAccess<T, U>(
)
}

/// Check that an expectation has passed after a condition has been evaluated
/// and throw an error if it failed, for nonescapable values.
///
/// This overload is used to conditionally unwrap optional values produced from
/// expanded property accesses:
///
/// ```swift
/// let z = try #require(x.nearestFoodTruck)
/// ```
///
/// - Warning: This function is used to implement the `#expect()` and
/// `#require()` macros. Do not call it directly.
@lifetime(copy lhs)
public func __checkPropertyAccess<T: ~Escapable, U: ~Escapable>(
_ lhs: T,
getting memberAccess: (T) -> U?,
expression: __Expression,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
sourceLocation: SourceLocation
) -> Result<U, any Error> {
let optionalValue = _overrideLifetime(memberAccess(lhs), copying: lhs)
return __checkValue(
optionalValue,
expression: expression,
expressionWithCapturedRuntimeValues:
expression.capturingRuntimeValues("<nonescapable>", "<nonescapable>"),
comments: comments(),
isRequired: isRequired,
sourceLocation: sourceLocation
)
}

// MARK: - Collection diffing

/// Check that an expectation has passed after a condition has been evaluated
Expand Down Expand Up @@ -756,6 +819,46 @@ public func __checkValue<T>(
}
}

/// Check that an expectation has passed after a condition has been evaluated
/// and throw an error if it failed, for a nonescapable type.
///
/// This overload is used to conditionally unwrap optional values:
///
/// ```swift
/// let x: Int? = ...
/// let y = try #require(x)
/// ```
///
/// - Warning: This function is used to implement the `#expect()` and
/// `#require()` macros. Do not call it directly.
@lifetime(copy optionalValue)
public func __checkValue<T: ~Escapable>(
_ optionalValue: T?,
expression: __Expression,
expressionWithCapturedRuntimeValues: @autoclosure () -> __Expression? = nil,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
sourceLocation: SourceLocation
) -> Result<T, any Error> {
let result = __checkValue(
optionalValue != nil,
expression: expression,
expressionWithCapturedRuntimeValues:
(expressionWithCapturedRuntimeValues() ?? expression)
.capturingRuntimeValue(optionalValue == nil ? "nil" : "<nonescapable>"),
comments: comments(),
isRequired: isRequired,
sourceLocation: sourceLocation
)
// Result.map doesn't support ~Escapable, so we do a manual conversion:
return switch result {
case .success:
.success(optionalValue.unsafelyUnwrapped)
case .failure(let error):
.failure(error)
}
}

/// Check that an expectation has passed after a condition has been evaluated
/// and throw an error if it failed.
///
Expand Down
3 changes: 2 additions & 1 deletion Sources/Testing/Support/Additions/ResultAdditions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

extension Result {
extension Result where Success: ~Escapable {
/// Handle this instance as if it were returned from a call to `#expect()`.
///
/// - Warning: This function is used to implement the `#expect()` and
Expand All @@ -19,6 +19,7 @@ extension Result {
///
/// - Warning: This function is used to implement the `#expect()` and
/// `#require()` macros. Do not call it directly.
@lifetime(copy self)
@inlinable public func __required() throws -> Success {
try get()
}
Expand Down
46 changes: 46 additions & 0 deletions Tests/TestingTests/MiscellaneousTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -600,4 +600,50 @@ struct MiscellaneousTests {
}
#expect(duration < .seconds(1))
}

@Test("Instance property of custom nonescapable type")
func nonEscapableValue() throws {
struct NonEscapableValue: ~Escapable {
private(set) var value: Int

@lifetime(borrow value)
init(_ value: Int) {
self.value = value
}

var isZero: Bool { value == 0 }
var isNonZero: Bool { value != 0 }
var nonZeroValue: Int? { value == 0 ? nil : value }

var incremented: NonEscapableValue? {
@lifetime(copy self)
get {
var result = self
result.value += 1
return result
}
}
}

let int = 2
let nev = NonEscapableValue(int)
#expect(!nev.isZero)
#expect(nev.isNonZero)
let v = try #require(nev.nonZeroValue)
#expect(v == int)

// The following errors due to the emitted code looking like:
//
// Testing.__checkPropertyAccess(nev.self, { $0.incremented }, ...)
//
// Since the closure is just declared as `(T) -> U?`, without any lifetime
// annotation saying that the `U?` result copies the lifetime of the
// input parameter `T`, compilation results in:
//
// error: Lifetime-dependent value escapes its scope
// note: It depends on the lifetime of argument '$0'
//
// let nev2 = try #require(nev.incremented)
// #expect(nev2.isNonZero)
}
}