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

Restructure @Dependency property and use BFS for DependencyManager #112

Merged
merged 18 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
150 changes: 86 additions & 64 deletions Sources/Spezi/Dependencies/DependencyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@

/// Collection of all modules with dependencies that are not yet processed.
private var modulesWithDependencies: [any Module]
/// Recursive search stack to keep track of potential circular dependencies.
private var searchStack: [any Module] = []


/// The current search stack for the module being pushed.
private var currentPushedModule: ModuleReference?
private var searchStacks: [ModuleReference: [any Module.Type]] = [:]


/// A ``DependencyManager`` in Spezi is used to gather information about modules with dependencies.
Expand All @@ -49,91 +52,71 @@

/// Resolves the dependency order.
///
/// After calling `resolve` you can safely access `initializedModules`.
/// After calling `resolve()` you can safely access `initializedModules`.
func resolve() {
// Start the dependency resolution on the first module.
while let nextModule = modulesWithDependencies.first {
push(nextModule)
}

injectDependencies()
assert(searchStacks.isEmpty, "`searchStacks` are not getting cleaned up!")
assert(currentPushedModule == nil, "`currentPushedModule` is never reset!")
}

private func injectDependencies() {
// We inject dependencies into existingModules as well as a new dependency might be an optional dependency from a existing module
// that wasn't previously injected.
for module in initializedModules + existingModules {
for dependency in module.dependencyDeclarations {
dependency.inject(from: self)
dependency.inject(from: self, for: module)
}
}
}

/// Push a module on the search stack and resolve dependency information.
private func push(_ module: any Module) {
searchStack.append(module)
assert(currentPushedModule == nil, "Module already pushed. Did the algorithm turn into an recursive one by accident?")

currentPushedModule = ModuleReference(module)
searchStacks[ModuleReference(module), default: []]
.append(type(of: module))

for dependency in module.dependencyDeclarations {
dependency.collect(into: self) // leads to calls to `require(_:defaultValue:)`
}
finishSearch(module)

finishSearch(for: module)
}

/// Communicate a requirement to a `DependencyManager`
/// - Parameters:
/// - dependency: The type of the dependency that should be resolved.
/// - defaultValue: A default instance of the dependency that is used when the `dependencyType` is not present in the `initializedModules` or `modulesWithDependencies`.
func require<M: Module>(_ dependency: M.Type, defaultValue: (() -> M)?) {
// 1. Return if the depending module is found in the `initializedModules` collection.
if initializedModules.contains(where: { type(of: $0) == M.self })
|| existingModules.contains(where: { type(of: $0) == M.self }) {
return
}

func require<M: Module>(_ dependency: M.Type, type dependencyType: DependencyType, defaultValue: (() -> M)?) {
testForSearchStackCycles(M.self)

// 2. Search for the required module is found in the `dependingModules` collection.
// If not, use the default value calling the `defaultValue` auto-closure.
guard let foundInModulesWithDependencies = modulesWithDependencies.first(where: { type(of: $0) == M.self }) else {
// 1. Check if it is actively requested to load this module.
if case .load = dependencyType {
guard let defaultValue else {
// optional dependency. The user didn't supply anything. So we can't deliver anything.
return
return // doesn't make sense, just ignore that

Check warning on line 101 in Sources/Spezi/Dependencies/DependencyManager.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Dependencies/DependencyManager.swift#L101

Added line #L101 was not covered by tests
}

let newModule = defaultValue()

implicitlyCreatedModules.insert(ModuleReference(newModule))
implicitlyCreate(defaultValue)
return
}

guard !newModule.dependencyDeclarations.isEmpty else {
initializedModules.append(newModule)
return
}

modulesWithDependencies.insert(newModule, at: 0)
push(newModule)

// 2. Return if the depending module is already initialized or being initialized.
if initializedModules.contains(where: { type(of: $0) == M.self })
|| existingModules.contains(where: { type(of: $0) == M.self })
|| modulesWithDependencies.contains(where: { type(of: $0) == M.self }) {
return
}

// Detect circles in the `recursiveSearch` collection.
guard !searchStack.contains(where: { type(of: $0) == M.self }) else {
let dependencyChain = searchStack
.map { String(describing: type(of: $0)) }
.joined(separator: ", ")

// The last element must exist as we entered the statement using a successful `contains` statement.
// There is not chance to recover here: If there is a crash here, we would fail in the precondition statement in the next line anyways
let lastElement = searchStack.last! // swiftlint:disable:this force_unwrapping
preconditionFailure(
"""
The `DependencyManager` has detected a dependency cycle of your Spezi modules.
The current dependency chain is: \(dependencyChain). The \(String(describing: type(of: lastElement))) required a type already present in the dependency chain.

Please ensure that the modules you use or develop can not trigger a dependency cycle.
"""
)


// 3. Otherwise, check if there is a default value we can implicitly load
if let defaultValue {
implicitlyCreate(defaultValue)
}

// If there is no cycle, resolve the dependencies of the module found in the `dependingModules`.
push(foundInModulesWithDependencies)
}

/// Retrieve a resolved dependency for a given type.
Expand All @@ -142,35 +125,74 @@
/// - module: The ``Module`` type to return.
/// - optional: Flag indicating if it is a optional return.
/// - Returns: Returns the Module instance. Only optional, if `optional` is set to `true` and no Module was found.
func retrieve<M: Module>(module: M.Type = M.self, optional: Bool = false) -> M? {
func retrieve<M: Module>(module: M.Type, type dependencyType: DependencyType, for owner: any Module) -> M? {
guard let candidate = existingModules.first(where: { type(of: $0) == M.self })
?? initializedModules.first(where: { type(of: $0) == M.self }),
let module = candidate as? M else {
precondition(optional, "Could not located dependency of type \(M.self)!")
precondition(
dependencyType.isOptional,
"""
'\(type(of: owner)) requires dependency of type '\(M.self)' which wasn't configured.
Please make sure this module is configured by including it in the configuration of your `SpeziAppDelegate` or following \
Module-specific instructions.
"""
)
return nil
}

return module
}

private func finishSearch(_ dependingModule: any Module) {
guard !searchStack.isEmpty else {
preconditionFailure("Internal logic error in the `DependencyManager`. Search Stack is empty.")
}
let module = searchStack.removeLast()
private func implicitlyCreate<M: Module>(_ module: () -> M) {
let newModule = module()

implicitlyCreatedModules.insert(ModuleReference(newModule))

if newModule.dependencyDeclarations.isEmpty {
initializedModules.append(newModule)
} else {
saveSearchStack(for: newModule)

guard module === dependingModule else {
preconditionFailure("Internal logic error in the `DependencyManager`. Search Stack element was not the one we are resolving for.")
modulesWithDependencies.append(newModule) // appending for BFS
}
}

private func finishSearch(for dependingModule: any Module) {
currentPushedModule = nil
searchStacks.removeValue(forKey: ModuleReference(dependingModule))


let dependingModulesCount = modulesWithDependencies.count
modulesWithDependencies.removeAll(where: { $0 === dependingModule })
precondition(
dependingModulesCount - 1 == modulesWithDependencies.count,
"Unexpected reduction of modules."
)
precondition(dependingModulesCount - 1 == modulesWithDependencies.count, "Unexpected reduction of modules.")

initializedModules.append(dependingModule)
}

private func saveSearchStack<M: Module>(for module: M) {
guard let currentPushedModule,
let searchStack = searchStacks[currentPushedModule] else {
return

Check warning on line 175 in Sources/Spezi/Dependencies/DependencyManager.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Dependencies/DependencyManager.swift#L175

Added line #L175 was not covered by tests
}

// propagate the search stack such that we have it available when we call push for this module
searchStacks[ModuleReference(module)] = searchStack
}

private func testForSearchStackCycles<M>(_ module: M.Type) {
if let currentPushedModule {
let searchStack = searchStacks[currentPushedModule, default: []]

precondition(
!searchStack.contains(where: { $0 == M.self }),
"""
The `DependencyManager` has detected a dependency cycle of your Spezi modules.
Supereg marked this conversation as resolved.
Show resolved Hide resolved
The current dependency chain is: \(searchStack.map { String(describing: $0) }.joined(separator: ", ")). \
The module '\(searchStack.last.unsafelyUnwrapped)' required '\(M.self)' which is contained in its own dependency chain.

Please ensure that the modules you use or develop can not trigger a dependency cycle.
"""
)
}
}
}
10 changes: 6 additions & 4 deletions Sources/Spezi/Dependencies/Property/DependencyBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
//


/// A result builder to build a ``DependencyCollection``.
/// A result builder to build a `DependencyCollection`.
///
/// For more information refer to ``DependencyCollection``.
@resultBuilder
public enum DependencyBuilder: DependencyCollectionBuilder {
/// An auto-closure expression, providing the default dependency value, building the ``DependencyCollection``.
public static func buildExpression<M: Module>(_ expression: @escaping @autoclosure () -> M) -> DependencyCollection {
DependencyCollection(DependencyContext(defaultValue: expression))
/// An auto-closure expression, providing the default dependency value, building the `DependencyCollection`.
public static func buildExpression<M: Module>(_ expression: M) -> DependencyCollection {
DependencyCollection(expression)
}
}
122 changes: 81 additions & 41 deletions Sources/Spezi/Dependencies/Property/DependencyCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,34 @@
//


/// A collection of dependency declarations.
public struct DependencyCollection: DependencyDeclaration {
let entries: [AnyDependencyContext]
/// A collection of dependencies.
///
/// This collection contains a collection of Modules that are meant to be declared as the dependencies of another module.
///
/// The code example below demonstrates how you can easily create your collection of dependencies from multiple different types of ``Module``s.
///
/// - Tip: You can also use ``append(contentsOf:)`` to combine two collections.
///
/// ```swift
/// var collection = DependencyCollection(ModuleA(), ModuleB(), ModuleC())
///
/// collection.append(ModuleD())
/// ```
///
/// - Note: Use the ``DependencyCollectionBuilder`` if you want to create your own result builder that can build a ``DependencyCollection`` component
/// out of multiple `Module` expressions.
public struct DependencyCollection {
private var entries: [AnyDependencyContext]

/// Determine if the collection is empty.
public var isEmpty: Bool {
entries.isEmpty
}

/// The count of entries.
public var count: Int {
entries.count
}

init(_ entries: [AnyDependencyContext]) {
self.entries = entries
Expand All @@ -18,56 +43,71 @@ public struct DependencyCollection: DependencyDeclaration {
init(_ entries: AnyDependencyContext...) {
self.init(entries)
}

/// Creates a ``DependencyCollection`` from a closure resulting in a single generic type conforming to the Spezi ``Module``.
/// - Parameters:
/// - type: The generic type resulting from the passed closure, has to conform to ``Module``.
/// - singleEntry: Closure returning a dependency conforming to ``Module``, stored within the ``DependencyCollection``.

/// Create an empty collection.
public init() {
self.entries = []
}


/// Create a collection with entries
///
/// ### Usage
/// - Note: You can create your own result builders that build a `DependencyCollection` using the ``DependencyCollectionBuilder``.
///
/// The `SomeCustomDependencyBuilder` enforces certain type constraints (e.g., `SomeTypeConstraint`, more specific than ``Module``) during aggregation of ``Module/Dependency``s (``Module``s) via a result builder.
/// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)-6ihsh``.
/// - Parameter entry: The parameter pack of modules.
public init<each M: Module>(_ entry: repeat each M) {
self.init()
repeat append(each entry)
}

/// Create a collection from a single entry closure.
///
/// ```swift
/// @resultBuilder
/// public enum SomeCustomDependencyBuilder: DependencyCollectionBuilder {
/// public static func buildExpression<T: SomeTypeConstraint>(_ expression: @escaping @autoclosure () -> T) -> DependencyCollection {
/// DependencyCollection(singleEntry: expression)
/// }
/// }
/// ```
/// - Note: You can create your own result builders that build a `DependencyCollection` using the ``DependencyCollectionBuilder``.
///
/// See `_DependencyPropertyWrapper/init(using:)` for a continued example regarding the usage of the implemented result builder.
/// - Parameters:
/// - type: The generic type resulting from the passed closure, has to conform to ``Module``.
/// - singleEntry: Closure returning a dependency conforming to ``Module``, stored within the ``DependencyCollection``.
@available(
*, deprecated, renamed: "init(_:)",
message: "DependencyCollection entries are now always an explicit request to load and do not require a closure anymore."
)
public init<Dependency: Module>(for type: Dependency.Type = Dependency.self, singleEntry: @escaping (() -> Dependency)) {
self.init(DependencyContext(for: type, defaultValue: singleEntry))
self.init(singleEntry())
}

/// Creates a ``DependencyCollection`` from a closure resulting in a single generic type conforming to the Spezi ``Module``.
/// Create a collection from a single entry closure.
///
/// - Note: You can create your own result builders that build a `DependencyCollection` using the ``DependencyCollectionBuilder``.
///
/// - Parameters:
/// - type: The generic type resulting from the passed closure, has to conform to ``Module``.
/// - singleEntry: Closure returning a dependency conforming to ``Module``, stored within the ``DependencyCollection``.
///
/// ### Usage
///
/// The `SomeCustomDependencyBuilder` enforces certain type constraints (e.g., `SomeTypeConstraint`, more specific than ``Module``) during aggregation of ``Module/Dependency``s (``Module``s) via a result builder.
/// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)-6nzui``.
///
/// ```swift
/// @resultBuilder
/// public enum SomeCustomDependencyBuilder: DependencyCollectionBuilder {
/// public static func buildExpression<T: SomeTypeConstraint>(_ expression: @escaping @autoclosure () -> T) -> DependencyCollection {
/// DependencyCollection(singleEntry: expression)
/// }
/// }
/// ```
///
/// See `_DependencyPropertyWrapper/init(using:)` for a continued example regarding the usage of the implemented result builder.
@available(
*, deprecated, renamed: "init(_:)",
message: "DependencyCollection entries are now always an explicit request to load and do not require a closure anymore."
)
public init<Dependency: Module>(for type: Dependency.Type = Dependency.self, singleEntry: @escaping @autoclosure (() -> Dependency)) {
self.init(singleEntry: singleEntry)
self.init(singleEntry())
}


/// Append a collection.
/// - Parameter collection: The dependency collection to append to this one.
public mutating func append(contentsOf collection: DependencyCollection) {
entries.append(contentsOf: collection.entries)
}


/// Append a module.
/// - Parameter module: The ``Module`` to append to the collection.
public mutating func append<M: Module>(_ module: M) {
// we always treat modules passed to a Dependency collection as an explicit request to load them, therefore .load
entries.append(DependencyContext(for: M.self, type: .load, defaultValue: { module }))
}
}


extension DependencyCollection: DependencyDeclaration {
func dependencyRelation(to module: DependencyReference) -> DependencyRelation {
let relations = entries.map { $0.dependencyRelation(to: module) }

Expand All @@ -87,9 +127,9 @@ public struct DependencyCollection: DependencyDeclaration {
}
}

func inject(from dependencyManager: DependencyManager) {
func inject(from dependencyManager: DependencyManager, for module: any Module) {
for entry in entries {
entry.inject(from: dependencyManager)
entry.inject(from: dependencyManager, for: module)
}
}

Expand Down
Loading
Loading