Skip to content
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

Reference coalescing #435

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion .github/workflows/pod_lib_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ jobs:
runs-on: macos-14
env:
DEVELOPER_DIR: /Applications/Xcode_15.4.app
strategy:
matrix:
platform: [macOS, iOS, tvOS, visionOS]
steps:
- uses: actions/checkout@v4
- run: bundle install --path vendor/bundle
- run: bundle exec pod lib lint --verbose
- if: matrix.platform == 'visionOS'
run: xcodebuild -downloadPlatform visionOS
- run: bundle exec pod lib lint --platforms=${{ matrix.platform }} --verbose
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,47 @@

##### Breaking

* None.

##### Enhancements

* Yams is able to coalesce references to objects decoded with YAML anchors.
[Adora Lynch](https://github.com/lynchsft)

##### Bug Fixes

* None.

## 5.3.0

##### Breaking

* None.

##### Enhancements

* Yams is able to encode and decode Anchors via YamlAnchorProviding, and
YamlAnchorCoding.
[Adora Lynch](https://github.com/lynchsft)
[#125](https://github.com/jpsim/Yams/issues/125)

* Yams is able to encode and decode Tags via YamlTagProviding
and YamlTagCoding.
[Adora Lynch](https://github.com/lynchsft)
[#265](https://github.com/jpsim/Yams/issues/265)

* Yams is able to detect redundant structs and automatically
alias them during encoding via RedundancyAliasingStrategy
[Adora Lynch](https://github.com/lynchsft)

##### Bug Fixes

* None.

## 5.2.0

##### Breaking

* Swift 5.7 or later is now required to build Yams.
[JP Simard](https://github.com/jpsim)

Expand Down
47 changes: 47 additions & 0 deletions Sources/Yams/AliasDereferencingStrategy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// AliasDereferencingStrategy.swift
// Yams
//
// Created by Adora Lynch on 8/9/24.
// Copyright (c) 2024 Yams. All rights reserved.
//

/// A class-bound protocol which implements a strategy for dereferencing aliases (or dealiasing) values during
/// YAML document decoding. YAML documents which do not contain anchors will not benefit from the use of
/// an AliasDereferencingStrategy in any way. The main use-case for dereferencing aliases in a YML document
/// is when decoding into class types. If the yaml document is large and contains many references
/// (perhaps it is a representation of a dense graph) then, decoding into structs will require the of large amounts
/// of system memory to represent highly redundant (duplicated) data structures.
/// However, if the same document is decoded into class types and the decoding uses
/// an `AliasDereferencingStrategy` such as `BasicAliasDereferencingStrategy` then the emitted value will have its
/// class references coalesced. No duplicate objects will be initialized (unless identical objects have multiple
/// distinct anchors in the YAML document). In some scenarios this may significantly reduce the memory footprint of
/// the decoded type.
public protocol AliasDereferencingStrategy: AnyObject {
/// The stored exestential type of all AliasDereferencingStrategys
typealias Value = (any Decodable)
/// get and set cached references, keyed bo an Anchor
subscript(_ key: Anchor) -> Value? { get set }
}

/// A AliasDereferencingStrategy which caches all values (even value-type values) in a Dictionary,
/// keyed by their Anchor.
/// For reference types, this strategy achieves reference coalescing
/// For value types, this strategy achieves short-cutting the decoding process when dereferencing aliases.
/// if the aliased structure is large, this may result in a time savings
public class BasicAliasDereferencingStrategy: AliasDereferencingStrategy {
/// Create a new BasicAliasDereferencingStrategy
public init() {}

private var map: [Anchor: Value] = .init()

/// get and set cached references, keyed bo an Anchor
public subscript(_ key: Anchor) -> Value? {
get { map[key] }
set { map[key] = newValue }
}
}

extension CodingUserInfoKey {
internal static let aliasDereferencingStrategy = Self(rawValue: "aliasDereferencingStrategy")!
}
40 changes: 40 additions & 0 deletions Sources/Yams/Anchor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// Anchor.swift
// Yams
//
// Created by Adora Lynch on 8/9/24.
// Copyright (c) 2024 Yams. All rights reserved.

import Foundation

/// A representation of a YAML tag see: https://yaml.org/spec/1.2.2/
/// Types interested in Encoding and Decoding Anchors should
/// conform to YamlAnchorProviding and YamlAnchorCoding respectively.
public final class Anchor: RawRepresentable, ExpressibleByStringLiteral, Codable, Hashable {

/// A CharacterSet containing only characters which are permitted by the underlying cyaml implementation
public static let permittedCharacters = CharacterSet.lowercaseLetters
.union(.uppercaseLetters)
.union(.decimalDigits)
.union(.init(charactersIn: "-_"))

/// Returns true if and only if `string` contains only characters which are also in `permittedCharacters`
public static func is_cyamlAlpha(_ string: String) -> Bool {
Anchor.permittedCharacters.isSuperset(of: .init(charactersIn: string))
}

public let rawValue: String

public init(rawValue: String) {
self.rawValue = rawValue
}

public init(stringLiteral value: String) {
rawValue = value
}
}

/// Conformance of Anchor to CustomStringConvertible returns `rawValue` as `description`
extension Anchor: CustomStringConvertible {
public var description: String { rawValue }
}
8 changes: 7 additions & 1 deletion Sources/Yams/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@

add_library(Yams
AliasDereferencingStrategy.swift
Anchor.swift
Constructor.swift
Decoder.swift
Emitter.swift
Encoder.swift
Mark.swift
Node.Alias.swift
Node.Mapping.swift
Node.Scalar.swift
Node.Sequence.swift
Node.swift
Parser.swift
RedundancyAliasingStrategy.swift
Representer.swift
Resolver.swift
String+Yams.swift
Tag.swift
YamlError.swift)
YamlAnchorProviding.swift
YamlError.swift
YamlTagProviding.swift)
target_compile_definitions(Yams PRIVATE
SWIFT_PACKAGE)
target_compile_options(Yams PRIVATE
Expand Down
18 changes: 10 additions & 8 deletions Sources/Yams/Constructor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ public final class Constructor {
if let method = mappingMap[node.tag.name], let result = method(mapping) {
return result
}
return [AnyHashable: Any]._construct_mapping(from: mapping)
return [AnyHashable: Any].private_construct_mapping(from: mapping)
case .sequence(let sequence):
if let method = sequenceMap[node.tag.name], let result = method(sequence) {
return result
}
return [Any].construct_seq(from: sequence)
case .alias:
preconditionFailure("Aliases should be resolved before construction")
}
}

Expand Down Expand Up @@ -270,7 +272,7 @@ extension ScalarConstructible where Self: FloatingPoint & SexagesimalConvertible
}

private extension FixedWidthInteger where Self: SexagesimalConvertible {
static func _construct(from scalar: Node.Scalar) -> Self? {
static func private_construct(from scalar: Node.Scalar) -> Self? {
guard scalar.style == .any || scalar.style == .plain else {
return nil
}
Expand Down Expand Up @@ -315,7 +317,7 @@ extension Int: ScalarConstructible {
///
/// - returns: An instance of `Int`, if one was successfully extracted from the scalar.
public static func construct(from scalar: Node.Scalar) -> Int? {
return _construct(from: scalar)
return private_construct(from: scalar)
}
}

Expand All @@ -328,7 +330,7 @@ extension UInt: ScalarConstructible {
///
/// - returns: An instance of `UInt`, if one was successfully extracted from the scalar.
public static func construct(from scalar: Node.Scalar) -> UInt? {
return _construct(from: scalar)
return private_construct(from: scalar)
}
}

Expand All @@ -341,7 +343,7 @@ extension Int64: ScalarConstructible {
///
/// - returns: An instance of `Int64`, if one was successfully extracted from the scalar.
public static func construct(from scalar: Node.Scalar) -> Int64? {
return _construct(from: scalar)
return private_construct(from: scalar)
}
}

Expand All @@ -354,7 +356,7 @@ extension UInt64: ScalarConstructible {
///
/// - returns: An instance of `UInt64`, if one was successfully extracted from the scalar.
public static func construct(from scalar: Node.Scalar) -> UInt64? {
return _construct(from: scalar)
return private_construct(from: scalar)
}
}

Expand Down Expand Up @@ -418,12 +420,12 @@ extension Dictionary {
///
/// - returns: An instance of `[AnyHashable: Any]`, if one was successfully extracted from the mapping.
public static func construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any]? {
return _construct_mapping(from: mapping)
return private_construct_mapping(from: mapping)
}
}

private extension Dictionary {
static func _construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any] {
static func private_construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any] {
let mapping = mapping.flatten()
// TODO: YAML supports keys other than str.
return [AnyHashable: Any](
Expand Down
Loading