Skip to content
Merged
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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.0"..<"602.0.0")
.package(url: "https://github.com/swiftlang/swift-syntax.git", "602.0.0" ..< "603.0.0")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
Expand Down
74 changes: 71 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,75 @@ extension ProfileView: Equatable {
lhs.id == rhs.id && lhs.username == rhs.username
}
}
```

## Isolation
`Equatable` macro supports generating the conformance with different isolation levels by using the `isolation` parameter.
The parameter accepts three values: `.nonisolated` (default), `.isolated`, and `.main` (requires Swift 6.2 or later).
The chosen isolation level will be applied to the generated conformances for both `Equatable` and `Hashable` (if applicable).

### Nonisolated (default)
The generated `Equatable` conformance is `nonisolated`, meaning it can be called from any context without isolation guarantees.
```swift
@Equatable(isolation: .nonisolated) (also omitting the parameter uses this mode)
struct Person {
let name: String
let age: Int
}
```

expands to:
```swift
extension Person: Equatable {
nonisolated public static func == (lhs: Person, rhs: Person) -> Bool {
lhs.name == rhs.name && lhs.age == rhs.age
}
}
```

### Isolated
The generated `Equatable` conformance is `isolated`, meaning it can only be called from within the actor's context.
```swift
@Equatable(isolation: .isolated)
struct Person {
let name: String
let age: Int
}
```

expands to:
```swift
extension Person: Equatable {
public static func == (lhs: Person, rhs: Person) -> Bool {
lhs.name == rhs.name && lhs.age == rhs.age
}
}
```

### Main (requires Swift 6.2 or later)
A common case is to have a `@MainActor` isolated type, SwiftUI views being a common example. Previously, the generated `Equatable` conformance had to be `nonisolated` in order to satisfy the protocol requirement.
This would then restrict us to access only nonisolated properties of the type in the generated `Equatable` function — which meant that we had to ignore all `@MainActor` isolated properties in the equality comparison.
Swift 6.2 introduced [isolated conformances](https://docs.swift.org/compiler/documentation/diagnostics/isolated-conformances/) allowing us to generate `Equatable` conformances
which are bound to the `@MainActor`. In this way the generated `Equatable` conformance can access `@MainActor` isolated properties of the type synchronously and the compiler will guarantee that the conformance
will be called only from the `@MainActor` context.

We can do so by specifying `@Equatable(isolation: .main)`, e.g:
```swift
@Equatable(isolation: .main)
@MainActor
struct Person {
let name: String
let age: Int
}
```

expands to:
```swift
extension Person: Equatable {
public static func == (lhs: Person, rhs: Person) -> Bool {
lhs.name == rhs.name && lhs.age == rhs.age
}
}
```

## Safety Considerations
Expand Down Expand Up @@ -148,9 +217,8 @@ import Equatable
@Equatable
struct User: Hashable {

let id: Int

@EquatableIgnored var name = ""
let id: Int
@EquatableIgnored var name = ""
}
```

Expand Down
85 changes: 84 additions & 1 deletion Sources/Equatable/Equatable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,91 @@
/// }
/// }
/// ```
///
///
/// ## Isolation
/// `Equatable` macro supports generating the conformance with different isolation levels by using the `isolation` parameter.
/// The parameter accepts three values: `.nonisolated` (default), `.isolated`, and `.main` (requires Swift 6.2 or later).
/// The chosen isolation level will be applied to the generated conformances for both `Equatable` and `Hashable` (if applicable).
///
/// ### Nonisolated (default)
/// The generated `Equatable` conformance is `nonisolated`, meaning it can be called from any context without isolation guarantees.
/// ```swift
/// @Equatable(isolation: .nonisolated) (also ommiting the parameter uses this mode)
/// struct Person {
/// let name: String
/// let age: Int
/// }
/// ```
///
/// expands to:
/// ```swift
/// extension Person: Equatable {
/// nonisolated public static func == (lhs: Person, rhs: Person) -> Bool {
/// lhs.name == rhs.name && lhs.age == rhs.age
/// }
/// }
/// ```
///
/// ### Isolated
/// The generated `Equatable` conformance is `isolated`, meaning it can only be called from within the actor's context.
/// ```swift
/// @Equatable(isolation: .isolated)
/// struct Person {
/// let name: String
/// let age: Int
/// }
/// ```
///
/// expands to:
/// ```swift
/// extension Person: Equatable {
/// public static func == (lhs: Person, rhs: Person) -> Bool {
/// lhs.name == rhs.name && lhs.age == rhs.age
/// }
/// }
/// ```
///
/// ### Main (requires Swift 6.2 or later)
/// A common case is to have a `@MainActor` isolated type, SwiftUI views being a common example. Previously, the generated `Equatable` conformance had to be `nonisolated` in order to satisfy the protocol requirement.
/// This would then restrict us to access only nonisolated properties of the type in the generated `Equatable` function — which ment that we had to ignore all `@MainActor` isolated properties in the equality comparison.
/// Swift 6.2 introduced [isolated confomances](https://docs.swift.org/compiler/documentation/diagnostics/isolated-conformances/) allowing us to generate `Equatable` confomances
/// which are bound to the `@MainActor`. In this way the generated `Equatable` conformance can access `@MainActor` isolated properties of the type synchonously and the compiler will guarantee that the confomance
/// will be called only from the `@MainActor` context.
///
/// We can do so by specifying `@Equatable(isolation: .main)`, e.g:
/// ```swift
/// @Equatable(isolation: .main)
/// @MainActor
/// struct Person {
/// let name: String
/// let age: Int
/// }
/// ```
///
/// expands to:
/// ```swift
/// extension Person: Equatable {
/// public static func == (lhs: Person, rhs: Person) -> Bool {
/// lhs.name == rhs.name && lhs.age == rhs.age
/// }
/// }
/// ```
///
@attached(extension, conformances: Equatable, Hashable, names: named(==), named(hash(into:)))
public macro Equatable() = #externalMacro(module: "EquatableMacros", type: "EquatableMacro")
public macro Equatable(isolation: Isolation = .nonisolated) = #externalMacro(module: "EquatableMacros", type: "EquatableMacro")

/// Isolation level for the generated Equatable functions.
public enum Isolation {
/// The generated `Equatable` conformance is `nonisolated`.
case nonisolated
/// The generated `Equatable` conformance is`isolated`.
case isolated
#if swift(>=6.2)
/// The generated `Equatable` conformance is `@MainActor` isolated.
case main
#endif
}

/// A peer macro that marks properties to be ignored in `Equatable` conformance generation.
///
Expand Down
Loading