-
Notifications
You must be signed in to change notification settings - Fork 51
[Runtime] Support recursive types #58
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
5e13b39
WIP on recursive types
czechboy0 d2b00a2
Make CopyOnWriteBox tests pass
czechboy0 791d068
Merge branch 'main' into hd-recursive-types
czechboy0 426ea03
Fix soundness
czechboy0 d77c98f
PR feedback: use the _modify accessor, wrap the ref storage in a struct
czechboy0 cf4ee66
Add inlinable to the generic cow box
czechboy0 9f1e5cd
Add more inlinables
czechboy0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftOpenAPIGenerator open source project | ||
// | ||
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
/// A type that wraps a value and enforces copy-on-write semantics. | ||
/// | ||
/// It also enables recursive types by introducing a "box" into the cycle, which | ||
/// allows the owning type to have a finite size. | ||
@_spi(Generated) | ||
public struct CopyOnWriteBox<Wrapped> { | ||
|
||
/// The reference type storage for the box. | ||
@usableFromInline | ||
internal final class Storage { | ||
|
||
/// The stored value. | ||
@usableFromInline | ||
var value: Wrapped | ||
|
||
/// Creates a new storage with the provided initial value. | ||
/// - Parameter value: The initial value to store in the box. | ||
@inlinable | ||
init(value: Wrapped) { | ||
self.value = value | ||
} | ||
} | ||
|
||
/// The internal storage of the box. | ||
@usableFromInline | ||
internal var storage: Storage | ||
|
||
/// Creates a new box. | ||
/// - Parameter value: The value to store in the box. | ||
@inlinable | ||
public init(value: Wrapped) { | ||
self.storage = .init(value: value) | ||
} | ||
|
||
/// The stored value whose accessors enforce copy-on-write semantics. | ||
@inlinable | ||
public var value: Wrapped { | ||
get { | ||
storage.value | ||
} | ||
_modify { | ||
if !isKnownUniquelyReferenced(&storage) { | ||
storage = Storage(value: storage.value) | ||
} | ||
yield &storage.value | ||
} | ||
czechboy0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
extension CopyOnWriteBox: Encodable where Wrapped: Encodable { | ||
|
||
/// Encodes this value into the given encoder. | ||
/// | ||
/// If the value fails to encode anything, `encoder` will encode an empty | ||
/// keyed container in its place. | ||
/// | ||
/// This function throws an error if any values are invalid for the given | ||
/// encoder's format. | ||
/// | ||
/// - Parameter encoder: The encoder to write data to. | ||
/// - Throws: On an encoding error. | ||
@inlinable | ||
public func encode(to encoder: any Encoder) throws { | ||
czechboy0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
try value.encode(to: encoder) | ||
} | ||
} | ||
|
||
extension CopyOnWriteBox: Decodable where Wrapped: Decodable { | ||
|
||
/// Creates a new instance by decoding from the given decoder. | ||
/// | ||
/// This initializer throws an error if reading from the decoder fails, or | ||
/// if the data read is corrupted or otherwise invalid. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Throws: On a decoding error. | ||
@inlinable | ||
public init(from decoder: any Decoder) throws { | ||
czechboy0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let value = try Wrapped(from: decoder) | ||
self.init(value: value) | ||
} | ||
} | ||
|
||
extension CopyOnWriteBox: Equatable where Wrapped: Equatable { | ||
|
||
/// Returns a Boolean value indicating whether two values are equal. | ||
/// | ||
/// Equality is the inverse of inequality. For any values `a` and `b`, | ||
/// `a == b` implies that `a != b` is `false`. | ||
/// | ||
/// - Parameters: | ||
/// - lhs: A value to compare. | ||
/// - rhs: Another value to compare. | ||
/// - Returns: A Boolean value indicating whether the values are equal. | ||
@inlinable | ||
public static func == ( | ||
lhs: CopyOnWriteBox<Wrapped>, | ||
rhs: CopyOnWriteBox<Wrapped> | ||
) -> Bool { | ||
lhs.value == rhs.value | ||
} | ||
} | ||
|
||
extension CopyOnWriteBox: Hashable where Wrapped: Hashable { | ||
|
||
/// Hashes the essential components of this value by feeding them into the | ||
/// given hasher. | ||
/// | ||
/// Implement this method to conform to the `Hashable` protocol. The | ||
/// components used for hashing must be the same as the components compared | ||
/// in your type's `==` operator implementation. Call `hasher.combine(_:)` | ||
/// with each of these components. | ||
/// | ||
/// - Important: In your implementation of `hash(into:)`, | ||
/// don't call `finalize()` on the `hasher` instance provided, | ||
/// or replace it with a different instance. | ||
/// Doing so may become a compile-time error in the future. | ||
/// | ||
/// - Parameter hasher: The hasher to use when combining the components | ||
/// of this instance. | ||
@inlinable | ||
public func hash(into hasher: inout Hasher) { | ||
czechboy0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
hasher.combine(value) | ||
} | ||
} | ||
|
||
extension CopyOnWriteBox: CustomStringConvertible where Wrapped: CustomStringConvertible { | ||
|
||
/// A textual representation of this instance. | ||
/// | ||
/// Calling this property directly is discouraged. Instead, convert an | ||
/// instance of any type to a string by using the `String(describing:)` | ||
/// initializer. This initializer works with any type, and uses the custom | ||
/// `description` property for types that conform to | ||
/// `CustomStringConvertible`: | ||
/// | ||
/// struct Point: CustomStringConvertible { | ||
/// let x: Int, y: Int | ||
/// | ||
/// var description: String { | ||
/// return "(\(x), \(y))" | ||
/// } | ||
/// } | ||
/// | ||
/// let p = Point(x: 21, y: 30) | ||
/// let s = String(describing: p) | ||
/// print(s) | ||
/// // Prints "(21, 30)" | ||
/// | ||
/// The conversion of `p` to a string in the assignment to `s` uses the | ||
/// `Point` type's `description` property. | ||
@inlinable | ||
public var description: String { | ||
value.description | ||
} | ||
} | ||
|
||
extension CopyOnWriteBox: CustomDebugStringConvertible where Wrapped: CustomDebugStringConvertible { | ||
|
||
/// A textual representation of this instance, suitable for debugging. | ||
/// | ||
/// Calling this property directly is discouraged. Instead, convert an | ||
/// instance of any type to a string by using the `String(reflecting:)` | ||
/// initializer. This initializer works with any type, and uses the custom | ||
/// `debugDescription` property for types that conform to | ||
/// `CustomDebugStringConvertible`: | ||
/// | ||
/// struct Point: CustomDebugStringConvertible { | ||
/// let x: Int, y: Int | ||
/// | ||
/// var debugDescription: String { | ||
/// return "(\(x), \(y))" | ||
/// } | ||
/// } | ||
/// | ||
/// let p = Point(x: 21, y: 30) | ||
/// let s = String(reflecting: p) | ||
/// print(s) | ||
/// // Prints "(21, 30)" | ||
/// | ||
/// The conversion of `p` to a string in the assignment to `s` uses the | ||
/// `Point` type's `debugDescription` property. | ||
@inlinable | ||
public var debugDescription: String { | ||
value.debugDescription | ||
} | ||
} | ||
|
||
extension CopyOnWriteBox: @unchecked Sendable where Wrapped: Sendable {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftOpenAPIGenerator open source project | ||
// | ||
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
import XCTest | ||
@_spi(Generated) import OpenAPIRuntime | ||
|
||
final class Test_CopyOnWriteBox: Test_Runtime { | ||
|
||
struct Node: Codable, Equatable { | ||
var id: Int | ||
var parent: CopyOnWriteBox<Node>? | ||
} | ||
|
||
func testModification() throws { | ||
var value = Node( | ||
id: 3, | ||
parent: .init( | ||
value: .init( | ||
id: 2 | ||
) | ||
) | ||
) | ||
XCTAssertEqual( | ||
value, | ||
Node( | ||
id: 3, | ||
parent: .init( | ||
value: .init( | ||
id: 2 | ||
) | ||
) | ||
) | ||
) | ||
value.parent!.value.parent = .init(value: .init(id: 1)) | ||
XCTAssertEqual( | ||
value, | ||
Node( | ||
id: 3, | ||
parent: .init( | ||
value: .init( | ||
id: 2, | ||
parent: .init( | ||
value: .init(id: 1) | ||
) | ||
) | ||
) | ||
) | ||
) | ||
} | ||
|
||
func testSerialization() throws { | ||
let value = CopyOnWriteBox(value: "Hello") | ||
try testRoundtrip( | ||
value, | ||
expectedJSON: #""Hello""# | ||
) | ||
} | ||
|
||
func testIntegration() throws { | ||
let value = Node( | ||
id: 3, | ||
parent: .init( | ||
value: .init( | ||
id: 2, | ||
parent: .init( | ||
value: .init(id: 1) | ||
) | ||
) | ||
) | ||
) | ||
try testRoundtrip( | ||
value, | ||
expectedJSON: #""" | ||
{ | ||
"id" : 3, | ||
"parent" : { | ||
"id" : 2, | ||
"parent" : { | ||
"id" : 1 | ||
} | ||
} | ||
} | ||
"""# | ||
) | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.