Skip to content

Commit a54c573

Browse files
authored
Add GeneratorEngine module with tests and macros (#31)
We'd like to avoid redundant rebuilding and regeneration for inputs that don't change. The new `GeneratorEngine` module achieves that by introducing a `Query` abstraction, which can be hashed and matched to its cached outputs if any are available. The new `Engine` actor can run such `Query` and immediately return a cached result file if one is present and its hash matches the recorded one. This required introducing new `@Query` and `@CacheKey` macros for making hashing easy and consistent on `QueryProtocol` types and their inputs. We can't use the standard `Hashable` protocol, as that produces new hashes on every process launch due to randomized hashing seeds. With Swift Crypto's `HashFunction` we can generate consistent hashes and record them in `SQLiteBackedCache`. To make the caching engine testable, I've introduced a new `FileSystem` actor protocol with `VirtualFileSystem` and `LocalFileSystem` actor implementations.
1 parent 5763434 commit a54c573

22 files changed

+1787
-5
lines changed

Docker/Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ARG swift_version=5.7
1+
ARG swift_version=5.9
22
ARG ubuntu_version=jammy
33
ARG base_image=swift:$swift_version-$ubuntu_version
44
FROM $base_image
@@ -7,7 +7,7 @@ ARG swift_version
77
ARG ubuntu_version
88

99
# set as UTF-8
10-
RUN apt-get update && apt-get install -y locales locales-all
10+
RUN apt-get update && apt-get install -y locales locales-all libsqlite3-dev
1111
ENV LC_ALL en_US.UTF-8
1212
ENV LANG en_US.UTF-8
1313
ENV LANGUAGE en_US.UTF-8

Package.resolved

+18
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@
4545
"version" : "1.0.4"
4646
}
4747
},
48+
{
49+
"identity" : "swift-crypto",
50+
"kind" : "remoteSourceControl",
51+
"location" : "https://github.com/apple/swift-crypto.git",
52+
"state" : {
53+
"revision" : "b51f1d6845b353a2121de1c6a670738ec33561a6",
54+
"version" : "3.1.0"
55+
}
56+
},
4857
{
4958
"identity" : "swift-log",
5059
"kind" : "remoteSourceControl",
@@ -99,6 +108,15 @@
99108
"version" : "1.19.0"
100109
}
101110
},
111+
{
112+
"identity" : "swift-syntax",
113+
"kind" : "remoteSourceControl",
114+
"location" : "https://github.com/apple/swift-syntax.git",
115+
"state" : {
116+
"revision" : "ffa3cd6fc2aa62adbedd31d3efaf7c0d86a9f029",
117+
"version" : "509.0.1"
118+
}
119+
},
102120
{
103121
"identity" : "swift-system",
104122
"kind" : "remoteSourceControl",

Package.swift

+43-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// swift-tools-version: 5.9
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

4+
import CompilerPluginSupport
45
import PackageDescription
56

67
let package = Package(
@@ -20,10 +21,12 @@ let package = Package(
2021
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.2"),
2122
.package(url: "https://github.com/apple/swift-async-algorithms.git", exact: "1.0.0-alpha"),
2223
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.0"),
24+
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
25+
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.1.0"),
2326
.package(url: "https://github.com/apple/swift-nio.git", from: "2.58.0"),
2427
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.19.0"),
2528
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"),
26-
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
29+
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.1"),
2730
],
2831
targets: [
2932
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -33,7 +36,7 @@ let package = Package(
3336
dependencies: [
3437
"SwiftSDKGenerator",
3538
.product(name: "ArgumentParser", package: "swift-argument-parser"),
36-
],
39+
],
3740
swiftSettings: [
3841
.enableExperimentalFeature("StrictConcurrency=complete"),
3942
]
@@ -44,7 +47,9 @@ let package = Package(
4447
.target(name: "AsyncProcess"),
4548
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
4649
.product(name: "AsyncHTTPClient", package: "async-http-client"),
50+
.product(name: "Logging", package: "swift-log"),
4751
.product(name: "SystemPackage", package: "swift-system"),
52+
"GeneratorEngine",
4853
],
4954
exclude: ["Dockerfiles"],
5055
swiftSettings: [
@@ -54,12 +59,47 @@ let package = Package(
5459
.testTarget(
5560
name: "SwiftSDKGeneratorTests",
5661
dependencies: [
57-
.target(name: "SwiftSDKGenerator"),
62+
"SwiftSDKGenerator",
5863
],
5964
swiftSettings: [
6065
.enableExperimentalFeature("StrictConcurrency=complete"),
6166
]
6267
),
68+
.target(
69+
name: "GeneratorEngine",
70+
dependencies: [
71+
.product(name: "AsyncHTTPClient", package: "async-http-client"),
72+
.product(name: "Crypto", package: "swift-crypto"),
73+
.product(name: "Logging", package: "swift-log"),
74+
.product(name: "SystemPackage", package: "swift-system"),
75+
"Macros",
76+
"SystemSQLite",
77+
]
78+
),
79+
.testTarget(
80+
name: "GeneratorEngineTests",
81+
dependencies: [
82+
"GeneratorEngine",
83+
]
84+
),
85+
.macro(
86+
name: "Macros",
87+
dependencies: [
88+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
89+
.product(name: "SwiftSyntax", package: "swift-syntax"),
90+
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
91+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
92+
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
93+
]
94+
),
95+
.testTarget(
96+
name: "MacrosTests",
97+
dependencies: [
98+
"Macros",
99+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
100+
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
101+
]
102+
),
63103
.systemLibrary(name: "SystemSQLite", pkgConfig: "sqlite3"),
64104
.target(
65105
name: "AsyncProcess",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Macros
14+
15+
@_exported import protocol Crypto.HashFunction
16+
import struct Foundation.URL
17+
import struct SystemPackage.FilePath
18+
19+
/// Indicates that values of a conforming type can be hashed with an arbitrary hashing function. Unlike `Hashable`,
20+
/// this protocol doesn't utilize random seed values and produces consistent hash values across process launches.
21+
public protocol CacheKeyProtocol {
22+
func hash(with hashFunction: inout some HashFunction)
23+
}
24+
25+
extension Bool: CacheKeyProtocol {
26+
public func hash(with hashFunction: inout some HashFunction) {
27+
"Swift.Bool".hash(with: &hashFunction)
28+
hashFunction.update(data: self ? [1] : [0])
29+
}
30+
}
31+
32+
extension Int: CacheKeyProtocol {
33+
public func hash(with hashFunction: inout some HashFunction) {
34+
"Swift.Int".hash(with: &hashFunction)
35+
withUnsafeBytes(of: self) {
36+
hashFunction.update(bufferPointer: $0)
37+
}
38+
}
39+
}
40+
41+
extension String: CacheKeyProtocol {
42+
public func hash(with hashFunction: inout some HashFunction) {
43+
var t = "Swift.String"
44+
t.withUTF8 {
45+
hashFunction.update(bufferPointer: .init($0))
46+
}
47+
var x = self
48+
x.withUTF8 {
49+
hashFunction.update(bufferPointer: .init($0))
50+
}
51+
}
52+
}
53+
54+
extension FilePath: CacheKeyProtocol {
55+
public func hash(with hashFunction: inout some HashFunction) {
56+
"SystemPackage.FilePath".hash(with: &hashFunction)
57+
self.string.hash(with: &hashFunction)
58+
}
59+
}
60+
61+
extension URL: CacheKeyProtocol {
62+
public func hash(with hashFunction: inout some HashFunction) {
63+
"Foundation.URL".hash(with: &hashFunction)
64+
self.description.hash(with: &hashFunction)
65+
}
66+
}
67+
68+
extension Optional: CacheKeyProtocol where Wrapped: CacheKeyProtocol {
69+
public func hash(with hashFunction: inout some HashFunction) {
70+
"Swift.Optional".hash(with: &hashFunction)
71+
if let self {
72+
true.hash(with: &hashFunction)
73+
self.hash(with: &hashFunction)
74+
} else {
75+
false.hash(with: &hashFunction)
76+
}
77+
}
78+
}
79+
80+
@attached(extension, conformances: CacheKeyProtocol, names: named(hash(with:)))
81+
public macro CacheKey() = #externalMacro(module: "Macros", type: "CacheKeyMacro")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import struct SystemPackage.FilePath
14+
15+
struct FileCacheRecord {
16+
let path: FilePath
17+
let hash: String
18+
}
19+
20+
extension FileCacheRecord: Codable {
21+
enum CodingKeys: CodingKey {
22+
case path
23+
case hash
24+
}
25+
26+
// FIXME: `Codable` on `FilePath` is broken
27+
init(from decoder: any Decoder) throws {
28+
let container = try decoder.container(keyedBy: CodingKeys.self)
29+
self.path = try FilePath(container.decode(String.self, forKey: .path))
30+
self.hash = try container.decode(String.self, forKey: .hash)
31+
}
32+
33+
func encode(to encoder: any Encoder) throws {
34+
var container = encoder.container(keyedBy: CodingKeys.self)
35+
try container.encode(self.path.string, forKey: .path)
36+
try container.encode(self.hash, forKey: .hash)
37+
}
38+
}

0 commit comments

Comments
 (0)