File: RequestEvaluator.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (78 lines) | stat: -rw-r--r-- 3,098 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
protocol EvaluationRequest: Hashable {
    associatedtype Output

    func evaluate(evaluator: Evaluator) throws -> Output
}

/// A central gate of computation that evaluates "requests" and caches their output, tracking dependencies graph.
/// This "Request-Evaluator" architecture allows to eliminate mutable state from AST, enable lazy-resolution, and
/// extremely simplifies cyclic-reference-detection.
///
/// This technique is heavily inspired by https://github.com/apple/swift/blob/main/docs/RequestEvaluator.md
internal class Evaluator {
    /// A cache that stores the result by request as a key
    private var cache: [AnyHashable: Result<Any, any Error>] = [:]
    /// A stack of current evaluating requests used to diagnostic report.
    /// The last element is the most recent request.
    private var activeRequests: [any EvaluationRequest] = []
    /// A set of current evaluating requests used for cyclic dependencies detection.
    private var activeRequestsSet: Set<AnyHashable> = []

    /// Create a new evaluator
    internal init() {}

    /// The entrypoint of the gate way, which evaluates the given request.
    /// - Parameter request: A request to be evaluated
    /// - Returns: Returns freshly-evaluated result if the request has never been evaluated yet.
    ///            Otherwise, returns the cached result.
    /// - Throws: Whatever is thrown by the `evaluate` method of the given request
    ///           and cyclic dependencies error if found.
    func evaluate<R: EvaluationRequest>(request: R) throws -> R.Output {
        let requestAsHashable = AnyHashable(request)
        if let cached = cache[requestAsHashable] {
            return try cached.get() as! R.Output
        }

        // Check cyclical request
        if activeRequestsSet.contains(requestAsHashable) {
            throw CyclicalRequestError(activeRequests: activeRequests + [request])
        }

        // Push the given request as an active request
        activeRequests.append(request)
        activeRequestsSet.insert(requestAsHashable)

        let result: Result<Any, any Error>
        defer {
            // Pop the request from active requests
            activeRequests.removeLast()
            activeRequestsSet.remove(requestAsHashable)

            // Cache the result by request as a key
            cache[requestAsHashable] = result
        }
        do {
            let output = try request.evaluate(evaluator: self)
            result = .success(output)
            return output
        } catch {
            result = .failure(error)
            throw error
        }
    }
}

extension Evaluator {
    struct CyclicalRequestError: Error, CustomStringConvertible {
        let activeRequests: [any EvaluationRequest]

        var description: String {
            var description = "==== Cycle detected! ====\n"
            for (index, request) in activeRequests.enumerated() {
                let indent = String(repeating: "  ", count: index)
                description += "\(indent)\\- \(request)\n"
            }
            return description
        }
    }
}