GCC Code Coverage Report


Directory: ../
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 79.4% 131 / 0 / 165
Functions: 90.0% 9 / 0 / 10
Branches: 45.3% 203 / 0 / 448

src/global/CacheManager.cpp
Line Branch Exec Source
1 // Copyright (c) 2021-2026 ChilliBits. All rights reserved.
2
3 #include "CacheManager.h"
4
5 #include <algorithm>
6 #include <cstring>
7 #include <fstream>
8 #include <queue>
9 #include <sstream>
10 #include <unordered_set>
11
12 #include <SourceFile.h>
13 #include <driver/Driver.h>
14 #include <global/GlobalResourceManager.h>
15 #include <util/SystemUtil.h>
16
17 #include <nlohmann/json.hpp>
18
19 namespace spice::compiler {
20
21 1391 CacheManager::CacheManager(const CliOptions &cliOptions) : cliOptions(cliOptions), cacheDir(cliOptions.cacheDir) {}
22
23 10356 std::string CacheManager::computeCacheKey(const std::string &sourceCode, const std::vector<std::string> &depCacheKeys) const {
24
1/2
✓ Branch 2 → 3 taken 10356 times.
✗ Branch 2 → 52 not taken.
10356 std::stringstream components;
25
2/4
✓ Branch 3 → 4 taken 10356 times.
✗ Branch 3 → 50 not taken.
✓ Branch 5 → 6 taken 10356 times.
✗ Branch 5 → 42 not taken.
10356 components << std::hex << std::hash<std::string>{}(sourceCode);
26
1/2
✓ Branch 6 → 7 taken 10356 times.
✗ Branch 6 → 50 not taken.
10356 components << static_cast<uint8_t>(cliOptions.buildMode);
27
1/2
✓ Branch 7 → 8 taken 10356 times.
✗ Branch 7 → 50 not taken.
10356 components << static_cast<uint8_t>(cliOptions.optLevel);
28
1/2
✓ Branch 8 → 9 taken 10356 times.
✗ Branch 8 → 50 not taken.
10356 components << static_cast<uint8_t>(cliOptions.instrumentation.sanitizer);
29
1/2
✓ Branch 9 → 10 taken 10356 times.
✗ Branch 9 → 50 not taken.
10356 components << static_cast<uint8_t>(cliOptions.instrumentation.debugInfoLevel);
30
1/2
✓ Branch 10 → 11 taken 10356 times.
✗ Branch 10 → 50 not taken.
10356 components << cliOptions.instrumentation.codeCoverage;
31
1/2
✓ Branch 12 → 13 taken 10356 times.
✗ Branch 12 → 50 not taken.
10356 components << cliOptions.targetTriple.str();
32
1/2
✓ Branch 13 → 14 taken 10356 times.
✗ Branch 13 → 50 not taken.
10356 components << cliOptions.useLTO;
33 // Frame pointers are a codegen decision baked into the emitted object, so an object built without them must not
34 // be reused once '--keep-frame-pointers' is passed, and vice versa.
35
1/2
✓ Branch 14 → 15 taken 10356 times.
✗ Branch 14 → 50 not taken.
10356 components << cliOptions.keepFramePointers;
36 // The output container influences codegen (PIC/PIE levels, DSO-local attributes for symbols,
37 // etc.), so reusing an object emitted for a different container would produce wrong output.
38
1/2
✓ Branch 15 → 16 taken 10356 times.
✗ Branch 15 → 50 not taken.
10356 components << static_cast<uint8_t>(cliOptions.outputContainer);
39 // Fold transitive dependency cache keys (sorted for determinism) into the key so that any
40 // change in a dependency invalidates every dependent's cache entry too.
41
1/2
✓ Branch 16 → 17 taken 10356 times.
✗ Branch 16 → 50 not taken.
10356 std::vector<std::string> sortedDepKeys = depCacheKeys;
42
1/2
✓ Branch 17 → 18 taken 10356 times.
✗ Branch 17 → 48 not taken.
10356 std::ranges::sort(sortedDepKeys);
43
2/2
✓ Branch 32 → 20 taken 5301 times.
✓ Branch 32 → 33 taken 10356 times.
26013 for (const std::string &depKey : sortedDepKeys)
44
1/2
✓ Branch 22 → 23 taken 5301 times.
✗ Branch 22 → 43 not taken.
5301 components << depKey;
45
2/4
✓ Branch 33 → 34 taken 10356 times.
✗ Branch 33 → 46 not taken.
✓ Branch 35 → 36 taken 10356 times.
✗ Branch 35 → 44 not taken.
31068 return std::to_string(std::hash<std::string>{}(components.str()));
46 10356 }
47
48 /**
49 * Derive the cache key of a module's object file from its source key and the generic manifestations emitted into it.
50 * Importers decide which instantiations exist, so the source key alone cannot tell such objects apart.
51 *
52 * @param sourceCacheKey Key from computeCacheKey()
53 * @param manifestations Fingerprint from Scope::collectManifestationFingerprint()
54 * @return Cache key of the object file
55 */
56 40 std::string CacheManager::foldManifestations(const std::string &sourceCacheKey, const std::stringstream &manifestations) {
57 // Combine both hashes instead of hashing a concatenation, which would copy the (potentially large) fingerprint again
58 constexpr std::hash<std::string> hasher;
59 40 size_t seed = hasher(sourceCacheKey);
60
1/2
✓ Branch 3 → 4 taken 40 times.
✗ Branch 3 → 10 not taken.
40 seed ^= hasher(manifestations.str()) + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2);
61
1/2
✓ Branch 6 → 7 taken 40 times.
✗ Branch 6 → 11 not taken.
80 return std::to_string(seed);
62 }
63
64 36 bool CacheManager::lookupSourceFile(SourceFile *sourceFile) const {
65
1/2
✓ Branch 2 → 3 taken 36 times.
✗ Branch 2 → 153 not taken.
36 const char *objectFileExtension = SystemUtil::getOutputFileExtension(cliOptions, OutputContainer::OBJECT_FILE);
66
3/6
✓ Branch 3 → 4 taken 36 times.
✗ Branch 3 → 103 not taken.
✓ Branch 4 → 5 taken 36 times.
✗ Branch 4 → 101 not taken.
✓ Branch 5 → 6 taken 36 times.
✗ Branch 5 → 99 not taken.
36 const std::filesystem::path metadataFilePath = cacheDir / (sourceFile->cacheKey + ".json");
67
4/8
✓ Branch 8 → 9 taken 36 times.
✗ Branch 8 → 111 not taken.
✓ Branch 9 → 10 taken 36 times.
✗ Branch 9 → 109 not taken.
✓ Branch 10 → 11 taken 36 times.
✗ Branch 10 → 107 not taken.
✓ Branch 11 → 12 taken 36 times.
✗ Branch 11 → 105 not taken.
36 const std::filesystem::path objectFilePath = cacheDir / (sourceFile->cacheKey + "." + objectFileExtension);
68
69 // Check if cache entry is available
70
7/10
✓ Branch 15 → 16 taken 36 times.
✗ Branch 15 → 149 not taken.
✓ Branch 16 → 17 taken 16 times.
✓ Branch 16 → 19 taken 20 times.
✓ Branch 17 → 18 taken 16 times.
✗ Branch 17 → 149 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 16 times.
✓ Branch 21 → 22 taken 20 times.
✓ Branch 21 → 23 taken 16 times.
36 if (!exists(metadataFilePath) || !exists(objectFilePath))
71 20 return false;
72
73 // Read metadata
74
1/2
✓ Branch 23 → 24 taken 16 times.
✗ Branch 23 → 149 not taken.
16 std::ifstream metadataFile(metadataFilePath);
75
2/4
✓ Branch 24 → 25 taken 16 times.
✗ Branch 24 → 147 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 16 times.
16 if (!metadataFile)
76 ✗ return false;
77
78 16 nlohmann::json metadata;
79 try {
80
1/2
✓ Branch 29 → 30 taken 16 times.
✗ Branch 29 → 114 not taken.
16 metadata = nlohmann::json::parse(metadataFile);
81 ✗ } catch (nlohmann::detail::parse_error &) {
82 ✗ return false;
83 ✗ }
84
85 // Verify all transitive dependency object files exist and collect their paths. We keep
86 // these even though Spice imports register themselves via their own concludeCompilation,
87 // because runtime modules (string-rt, memory-rt, ...) are pulled in implicitly during
88 // symbol-table building - a stage that gets skipped for cache-restored files. Without
89 // this list those runtime objects would be missing from the link. The linker dedupes the
90 // overlap with deps that did register themselves.
91
2/4
✓ Branch 33 → 34 taken 16 times.
✗ Branch 33 → 145 not taken.
✓ Branch 34 → 35 taken 16 times.
✗ Branch 34 → 65 not taken.
16 if (metadata.contains("dependencies")) {
92
6/10
✓ Branch 35 → 36 taken 16 times.
✗ Branch 35 → 136 not taken.
✓ Branch 38 → 39 taken 10 times.
✗ Branch 38 → 135 not taken.
✓ Branch 60 → 62 taken 10 times.
✗ Branch 60 → 136 not taken.
✓ Branch 62 → 63 taken 26 times.
✗ Branch 62 → 136 not taken.
✓ Branch 63 → 38 taken 10 times.
✓ Branch 63 → 64 taken 16 times.
26 for (const auto &depKey : metadata["dependencies"]) {
93
1/2
✓ Branch 39 → 40 taken 10 times.
✗ Branch 39 → 135 not taken.
10 const std::string key = depKey.get<std::string>();
94
4/8
✓ Branch 40 → 41 taken 10 times.
✗ Branch 40 → 128 not taken.
✓ Branch 41 → 42 taken 10 times.
✗ Branch 41 → 126 not taken.
✓ Branch 42 → 43 taken 10 times.
✗ Branch 42 → 124 not taken.
✓ Branch 43 → 44 taken 10 times.
✗ Branch 43 → 122 not taken.
10 const std::filesystem::path depObjectFilePath = cacheDir / (key + "." + objectFileExtension);
95
2/4
✓ Branch 47 → 48 taken 10 times.
✗ Branch 47 → 131 not taken.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 10 times.
10 if (!exists(depObjectFilePath))
96 ✗ return false;
97
1/2
✓ Branch 50 → 51 taken 10 times.
✗ Branch 50 → 131 not taken.
10 sourceFile->cachedObjectFilePaths.push_back(depObjectFilePath);
98
2/4
✓ Branch 53 → 54 taken 10 times.
✗ Branch 53 → 55 not taken.
✓ Branch 58 → 59 taken 10 times.
✗ Branch 58 → 61 not taken.
20 }
99 }
100
101 // Add this file's own object file last
102
1/2
✓ Branch 65 → 66 taken 16 times.
✗ Branch 65 → 145 not taken.
16 sourceFile->cachedObjectFilePaths.push_back(objectFilePath);
103
104 // Restore linker flags and additional source paths
105
2/4
✓ Branch 66 → 67 taken 16 times.
✗ Branch 66 → 145 not taken.
✓ Branch 67 → 68 taken 16 times.
✗ Branch 67 → 79 not taken.
16 if (metadata.contains("linkerFlags"))
106
3/8
✓ Branch 68 → 69 taken 16 times.
✗ Branch 68 → 140 not taken.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 140 not taken.
✓ Branch 76 → 77 taken 16 times.
✗ Branch 76 → 140 not taken.
✗ Branch 77 → 71 not taken.
✓ Branch 77 → 78 taken 16 times.
16 for (const auto &flag : metadata["linkerFlags"])
107 ✗ sourceFile->sourceLinkerFlags.push_back(flag.get<std::string>());
108
2/4
✓ Branch 79 → 80 taken 16 times.
✗ Branch 79 → 145 not taken.
✓ Branch 80 → 81 taken 16 times.
✗ Branch 80 → 92 not taken.
16 if (metadata.contains("additionalSourcePaths"))
109
3/8
✓ Branch 81 → 82 taken 16 times.
✗ Branch 81 → 144 not taken.
✗ Branch 84 → 85 not taken.
✗ Branch 84 → 144 not taken.
✓ Branch 89 → 90 taken 16 times.
✗ Branch 89 → 144 not taken.
✗ Branch 90 → 84 not taken.
✓ Branch 90 → 91 taken 16 times.
16 for (const auto &path : metadata["additionalSourcePaths"])
110 ✗ sourceFile->sourceAdditionalSourcePaths.emplace_back(path.get<std::string>());
111
112 16 return true;
113 36 }
114
115 20 void CacheManager::cacheSourceFile(const SourceFile *sourceFile) const {
116 // Don't cache if LTO is enabled and this isn't the main file (no object file produced)
117
1/4
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 20 times.
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 5 not taken.
20 if (cliOptions.useLTO && !sourceFile->isMainFile)
118 ✗ return;
119
120 // Determine the source object file path (mirrors runObjectEmitter's path logic)
121
2/4
✓ Branch 5 → 6 taken 20 times.
✗ Branch 5 → 174 not taken.
✓ Branch 6 → 7 taken 20 times.
✗ Branch 6 → 172 not taken.
20 std::filesystem::path sourceObjFilePath = cliOptions.outputDir / sourceFile->filePath.filename();
122
2/4
✓ Branch 8 → 9 taken 20 times.
✗ Branch 8 → 177 not taken.
✓ Branch 9 → 10 taken 20 times.
✗ Branch 9 → 175 not taken.
20 sourceObjFilePath.replace_extension("o");
123
124 // Determine cache paths
125
1/2
✓ Branch 11 → 12 taken 20 times.
✗ Branch 11 → 247 not taken.
20 const char *objectFileExtension = SystemUtil::getOutputFileExtension(cliOptions, OutputContainer::OBJECT_FILE);
126
4/8
✓ Branch 12 → 13 taken 20 times.
✗ Branch 12 → 184 not taken.
✓ Branch 13 → 14 taken 20 times.
✗ Branch 13 → 182 not taken.
✓ Branch 14 → 15 taken 20 times.
✗ Branch 14 → 180 not taken.
✓ Branch 15 → 16 taken 20 times.
✗ Branch 15 → 178 not taken.
20 const std::filesystem::path cachedObjectFilePath = cacheDir / (sourceFile->cacheKey + "." + objectFileExtension);
127
3/6
✓ Branch 19 → 20 taken 20 times.
✗ Branch 19 → 191 not taken.
✓ Branch 20 → 21 taken 20 times.
✗ Branch 20 → 189 not taken.
✓ Branch 21 → 22 taken 20 times.
✗ Branch 21 → 187 not taken.
20 const std::filesystem::path metadataFilePath = cacheDir / (sourceFile->cacheKey + ".json");
128
129 // Verify source object file exists
130 20 std::error_code error;
131
3/6
✓ Branch 26 → 27 taken 20 times.
✗ Branch 26 → 29 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 20 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 20 times.
20 if (!std::filesystem::exists(sourceObjFilePath, error) || error)
132 ✗ return;
133
134 // Ensure cache directory exists
135
1/2
✓ Branch 33 → 34 taken 20 times.
✗ Branch 33 → 243 not taken.
20 std::filesystem::create_directories(cacheDir, error);
136
1/2
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 20 times.
20 if (error)
137 ✗ return;
138
139 // Copy object file to cache
140
1/2
✓ Branch 37 → 38 taken 20 times.
✗ Branch 37 → 243 not taken.
20 std::filesystem::copy_file(sourceObjFilePath, cachedObjectFilePath, std::filesystem::copy_options::overwrite_existing, error);
141
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 20 times.
20 if (error)
142 ✗ return;
143
144 // Collect all transitive dependency cache keys, linker flags, and additional source paths.
145 // We need the transitive list so that cache-restored files can replay the full linker input
146 // even for implicit deps (e.g. runtime modules requested during symbol-table building, which
147 // a cache hit skips). The linker dedupes the overlap with deps that register themselves.
148 20 std::vector<std::string> depCacheKeys;
149
1/2
✓ Branch 41 → 42 taken 20 times.
✗ Branch 41 → 241 not taken.
20 std::vector<std::string> allLinkerFlags = sourceFile->sourceLinkerFlags;
150 20 std::vector<std::string> allAdditionalSourcePaths;
151
1/2
✗ Branch 58 → 44 not taken.
✓ Branch 58 → 59 taken 20 times.
40 for (const auto &p : sourceFile->sourceAdditionalSourcePaths)
152 ✗ allAdditionalSourcePaths.push_back(p.string());
153 20 std::unordered_set<std::string> visited;
154
1/2
✓ Branch 60 → 61 taken 20 times.
✗ Branch 60 → 235 not taken.
20 std::queue<const SourceFile *> worklist;
155
5/8
✓ Branch 61 → 62 taken 20 times.
✗ Branch 61 → 197 not taken.
✓ Branch 62 → 63 taken 20 times.
✗ Branch 62 → 197 not taken.
✓ Branch 63 → 64 taken 20 times.
✗ Branch 63 → 197 not taken.
✓ Branch 69 → 65 taken 12 times.
✓ Branch 69 → 70 taken 20 times.
32 for (const SourceFile *dep : sourceFile->dependencies | std::views::values)
156
1/2
✓ Branch 66 → 67 taken 12 times.
✗ Branch 66 → 197 not taken.
12 worklist.push(dep);
157
2/2
✓ Branch 113 → 71 taken 14 times.
✓ Branch 113 → 114 taken 20 times.
34 while (!worklist.empty()) {
158 14 const SourceFile *dep = worklist.front();
159 14 worklist.pop();
160
2/4
✓ Branch 73 → 74 taken 14 times.
✗ Branch 73 → 233 not taken.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 14 times.
14 if (!visited.insert(dep->cacheKey).second)
161 ✗ continue;
162
1/2
✓ Branch 76 → 77 taken 14 times.
✗ Branch 76 → 233 not taken.
14 depCacheKeys.push_back(dep->cacheKey);
163
1/2
✓ Branch 83 → 84 taken 14 times.
✗ Branch 83 → 198 not taken.
28 allLinkerFlags.insert(allLinkerFlags.end(), dep->sourceLinkerFlags.begin(), dep->sourceLinkerFlags.end());
164
1/2
✗ Branch 100 → 86 not taken.
✓ Branch 100 → 101 taken 14 times.
28 for (const auto &p : dep->sourceAdditionalSourcePaths)
165 ✗ allAdditionalSourcePaths.push_back(p.string());
166
5/8
✓ Branch 101 → 102 taken 14 times.
✗ Branch 101 → 204 not taken.
✓ Branch 102 → 103 taken 14 times.
✗ Branch 102 → 204 not taken.
✓ Branch 103 → 104 taken 14 times.
✗ Branch 103 → 204 not taken.
✓ Branch 109 → 105 taken 2 times.
✓ Branch 109 → 110 taken 14 times.
16 for (const SourceFile *transitiveDep : dep->dependencies | std::views::values)
167
1/2
✓ Branch 106 → 107 taken 2 times.
✗ Branch 106 → 204 not taken.
2 worklist.push(transitiveDep);
168 }
169
170 // Write metadata file
171 20 nlohmann::json metadata;
172
3/6
✓ Branch 115 → 116 taken 20 times.
✗ Branch 115 → 209 not taken.
✓ Branch 116 → 117 taken 20 times.
✗ Branch 116 → 207 not taken.
✓ Branch 117 → 118 taken 20 times.
✗ Branch 117 → 205 not taken.
20 metadata["sourceFile"] = sourceFile->filePath.string();
173
2/4
✓ Branch 121 → 122 taken 20 times.
✗ Branch 121 → 213 not taken.
✓ Branch 122 → 123 taken 20 times.
✗ Branch 122 → 211 not taken.
20 metadata["fileName"] = sourceFile->fileName;
174
2/4
✓ Branch 125 → 126 taken 20 times.
✗ Branch 125 → 216 not taken.
✓ Branch 126 → 127 taken 20 times.
✗ Branch 126 → 214 not taken.
20 metadata["cacheKey"] = sourceFile->cacheKey;
175
2/4
✓ Branch 129 → 130 taken 20 times.
✗ Branch 129 → 219 not taken.
✓ Branch 130 → 131 taken 20 times.
✗ Branch 130 → 217 not taken.
20 metadata["dependencies"] = depCacheKeys;
176
2/4
✓ Branch 133 → 134 taken 20 times.
✗ Branch 133 → 222 not taken.
✓ Branch 134 → 135 taken 20 times.
✗ Branch 134 → 220 not taken.
20 metadata["linkerFlags"] = allLinkerFlags;
177
2/4
✓ Branch 137 → 138 taken 20 times.
✗ Branch 137 → 225 not taken.
✓ Branch 138 → 139 taken 20 times.
✗ Branch 138 → 223 not taken.
20 metadata["additionalSourcePaths"] = allAdditionalSourcePaths;
178
1/2
✓ Branch 141 → 142 taken 20 times.
✗ Branch 141 → 231 not taken.
20 std::ofstream metadataStream(metadataFilePath);
179
2/4
✓ Branch 142 → 143 taken 20 times.
✗ Branch 142 → 229 not taken.
✓ Branch 143 → 144 taken 20 times.
✗ Branch 143 → 148 not taken.
20 if (metadataStream)
180
2/4
✓ Branch 144 → 145 taken 20 times.
✗ Branch 144 → 228 not taken.
✓ Branch 145 → 146 taken 20 times.
✗ Branch 145 → 226 not taken.
20 metadataStream << metadata.dump();
181
3/6
✓ Branch 157 → 158 taken 20 times.
✗ Branch 157 → 159 not taken.
✓ Branch 162 → 163 taken 20 times.
✗ Branch 162 → 164 not taken.
✓ Branch 167 → 168 taken 20 times.
✗ Branch 167 → 170 not taken.
60 }
182
183 // Hash the content of a single linker input that's not produced by the Spice cache itself
184 // (e.g. C/C++ files referenced via @core.linker.additionalSource). Returns a sentinel that
185 // folds the path in if the file can't be opened, so a vanished file still produces a stable
186 // (but different) cache key.
187 6 std::string hashLinkedFile(const std::filesystem::path &path) {
188
1/2
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 33 not taken.
6 std::ifstream stream(path, std::ios::binary);
189
2/4
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 31 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 10 taken 6 times.
6 if (!stream)
190 ✗ return "missing:" + path.string();
191
1/2
✓ Branch 10 → 11 taken 6 times.
✗ Branch 10 → 31 not taken.
6 std::stringstream content;
192
1/2
✓ Branch 12 → 13 taken 6 times.
✗ Branch 12 → 29 not taken.
6 content << stream.rdbuf();
193
2/4
✓ Branch 13 → 14 taken 6 times.
✗ Branch 13 → 27 not taken.
✓ Branch 15 → 16 taken 6 times.
✗ Branch 15 → 25 not taken.
12 return std::to_string(std::hash<std::string>{}(content.str()));
194 6 }
195
196 32 std::string computeExecutableCacheKey(const std::vector<std::string> &objectFileCacheKeys,
197 const std::vector<std::string> &linkerFlags,
198 const std::vector<std::filesystem::path> &additionalSourcePaths,
199 const CliOptions &cliOptions) {
200
1/2
✓ Branch 2 → 3 taken 32 times.
✗ Branch 2 → 84 not taken.
32 std::stringstream components;
201
2/2
✓ Branch 17 → 5 taken 42 times.
✓ Branch 17 → 18 taken 32 times.
106 for (const std::string &key : objectFileCacheKeys)
202
1/2
✓ Branch 7 → 8 taken 42 times.
✗ Branch 7 → 67 not taken.
42 components << key;
203
2/2
✓ Branch 32 → 20 taken 32 times.
✓ Branch 32 → 33 taken 32 times.
96 for (const std::string &flag : linkerFlags)
204
1/2
✓ Branch 22 → 23 taken 32 times.
✗ Branch 22 → 68 not taken.
32 components << flag;
205 // Sort additional source paths so traversal order doesn't perturb the key, then fold in
206 // path + content hash. Without this, edits to a referenced C/C++ source would leave every
207 // Spice object cache key unchanged, and we'd serve a stale executable.
208
1/2
✓ Branch 33 → 34 taken 32 times.
✗ Branch 33 → 82 not taken.
32 std::vector<std::filesystem::path> sortedAdditionalSources = additionalSourcePaths;
209
1/2
✓ Branch 34 → 35 taken 32 times.
✗ Branch 34 → 80 not taken.
32 std::ranges::sort(sortedAdditionalSources);
210
2/2
✓ Branch 55 → 37 taken 6 times.
✓ Branch 55 → 56 taken 32 times.
70 for (const std::filesystem::path &additionalSource : sortedAdditionalSources)
211
5/10
✓ Branch 39 → 40 taken 6 times.
✗ Branch 39 → 74 not taken.
✓ Branch 40 → 41 taken 6 times.
✗ Branch 40 → 72 not taken.
✓ Branch 41 → 42 taken 6 times.
✗ Branch 41 → 72 not taken.
✓ Branch 42 → 43 taken 6 times.
✗ Branch 42 → 71 not taken.
✓ Branch 43 → 44 taken 6 times.
✗ Branch 43 → 69 not taken.
6 components << additionalSource.string() << '\0' << hashLinkedFile(additionalSource);
212
1/2
✓ Branch 56 → 57 taken 32 times.
✗ Branch 56 → 80 not taken.
32 components << static_cast<uint8_t>(cliOptions.outputContainer);
213
1/2
✓ Branch 57 → 58 taken 32 times.
✗ Branch 57 → 80 not taken.
32 components << cliOptions.staticLinking;
214
2/4
✓ Branch 58 → 59 taken 32 times.
✗ Branch 58 → 78 not taken.
✓ Branch 60 → 61 taken 32 times.
✗ Branch 60 → 76 not taken.
96 return std::to_string(std::hash<std::string>{}(components.str()));
215 32 }
216
217 18 bool CacheManager::lookupExecutable(const std::vector<std::string> &objectFileCacheKeys,
218 const std::vector<std::string> &linkerFlags,
219 const std::vector<std::filesystem::path> &additionalSourcePaths,
220 std::filesystem::path &cachedExecutablePath) const {
221
1/2
✓ Branch 2 → 3 taken 18 times.
✗ Branch 2 → 61 not taken.
18 const std::string execCacheKey = computeExecutableCacheKey(objectFileCacheKeys, linkerFlags, additionalSourcePaths, cliOptions);
222
223 // Determine expected extension
224
1/2
✓ Branch 3 → 4 taken 18 times.
✗ Branch 3 → 59 not taken.
18 const char *extension = SystemUtil::getOutputFileExtension(cliOptions, cliOptions.outputContainer);
225
12/22
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 9 taken 16 times.
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 37 not taken.
✓ Branch 8 → 12 taken 2 times.
✗ Branch 8 → 37 not taken.
✓ Branch 11 → 12 taken 16 times.
✗ Branch 11 → 37 not taken.
✓ Branch 12 → 13 taken 18 times.
✗ Branch 12 → 35 not taken.
✓ Branch 14 → 15 taken 16 times.
✓ Branch 14 → 17 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 19 taken 16 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 22 taken 16 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 40 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 49 not taken.
52 const std::string fileName = execCacheKey + (strlen(extension) > 0 ? "." + std::string(extension) : "");
226
2/4
✓ Branch 22 → 23 taken 18 times.
✗ Branch 22 → 54 not taken.
✓ Branch 23 → 24 taken 18 times.
✗ Branch 23 → 52 not taken.
18 const std::filesystem::path execPath = cacheDir / fileName;
227
228
3/4
✓ Branch 25 → 26 taken 18 times.
✗ Branch 25 → 55 not taken.
✓ Branch 26 → 27 taken 14 times.
✓ Branch 26 → 28 taken 4 times.
18 if (!exists(execPath))
229 14 return false;
230
231
1/2
✓ Branch 28 → 29 taken 4 times.
✗ Branch 28 → 55 not taken.
4 cachedExecutablePath = execPath;
232 4 return true;
233 18 }
234
235 14 void CacheManager::cacheExecutable(const std::vector<std::string> &objFileCacheKeys, const std::vector<std::string> &linkerFlags,
236 const std::vector<std::filesystem::path> &additionalSourcePaths,
237 const std::filesystem::path &executablePath) const {
238
1/2
✓ Branch 2 → 3 taken 14 times.
✗ Branch 2 → 82 not taken.
14 const std::string execCacheKey = computeExecutableCacheKey(objFileCacheKeys, linkerFlags, additionalSourcePaths, cliOptions);
239
240 // Determine cached file name
241
1/2
✓ Branch 3 → 4 taken 14 times.
✗ Branch 3 → 80 not taken.
14 const char *extension = SystemUtil::getOutputFileExtension(cliOptions, cliOptions.outputContainer);
242
6/22
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 9 taken 14 times.
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 58 not taken.
✗ Branch 8 → 12 not taken.
✗ Branch 8 → 58 not taken.
✓ Branch 11 → 12 taken 14 times.
✗ Branch 11 → 58 not taken.
✓ Branch 12 → 13 taken 14 times.
✗ Branch 12 → 56 not taken.
✓ Branch 14 → 15 taken 14 times.
✗ Branch 14 → 17 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 14 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 22 taken 14 times.
✗ Branch 58 → 59 not taken.
✗ Branch 58 → 61 not taken.
✗ Branch 63 → 64 not taken.
✗ Branch 63 → 65 not taken.
✗ Branch 67 → 68 not taken.
✗ Branch 67 → 70 not taken.
42 const std::string fileName = execCacheKey + (strlen(extension) > 0 ? "." + std::string(extension) : "");
243
2/4
✓ Branch 22 → 23 taken 14 times.
✗ Branch 22 → 75 not taken.
✓ Branch 23 → 24 taken 14 times.
✗ Branch 23 → 73 not taken.
14 const std::filesystem::path cachedExecPath = cacheDir / fileName;
244
245 // Verify executable exists
246 14 std::error_code error;
247
5/6
✓ Branch 27 → 28 taken 12 times.
✓ Branch 27 → 30 taken 2 times.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 12 times.
✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 34 taken 12 times.
14 if (!std::filesystem::exists(executablePath, error) || error)
248 2 return;
249
250 // Ensure cache directory exists
251
1/2
✓ Branch 34 → 35 taken 12 times.
✗ Branch 34 → 76 not taken.
12 std::filesystem::create_directories(cacheDir, error);
252
1/2
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 12 times.
12 if (error)
253 ✗ return;
254
255 // Copy executable to cache
256
1/2
✓ Branch 38 → 39 taken 12 times.
✗ Branch 38 → 76 not taken.
12 std::filesystem::copy_file(executablePath, cachedExecPath, std::filesystem::copy_options::overwrite_existing, error);
257
6/6
✓ Branch 41 → 42 taken 12 times.
✓ Branch 41 → 43 taken 2 times.
✓ Branch 46 → 47 taken 12 times.
✓ Branch 46 → 48 taken 2 times.
✓ Branch 51 → 52 taken 12 times.
✓ Branch 51 → 54 taken 2 times.
42 }
258
259 ✗ void CacheManager::linkOrRestoreExecutable(GlobalResourceManager &resourceManager) const {
260 ✗ const ExternalLinkerInterface &linker = resourceManager.linker;
261
262 // Collect object file cache keys and any external linker inputs (e.g. C/C++ files added
263 // via @core.linker.additionalSource) that participate in the executable cache key.
264 ✗ std::vector<std::string> objectFileCacheKeys;
265 ✗ std::vector<std::filesystem::path> additionalSourcePaths;
266 ✗ for (const auto &sourceFile : resourceManager.sourceFiles | std::views::values) {
267 ✗ objectFileCacheKeys.push_back(sourceFile->cacheKey);
268 ✗ for (const std::filesystem::path &additionalSource : sourceFile->sourceAdditionalSourcePaths)
269 ✗ additionalSourcePaths.push_back(additionalSource);
270 }
271
272 // Check if we have a cached executable
273 ✗ std::filesystem::path cachedExecutablePath;
274 ✗ if (!cliOptions.ignoreCache &&
275 ✗ lookupExecutable(objectFileCacheKeys, linker.getLinkerFlags(), additionalSourcePaths, cachedExecutablePath)) {
276 // Restore cached executable
277 ✗ std::error_code ec;
278 ✗ std::filesystem::create_directories(linker.outputPath.parent_path(), ec);
279 ✗ std::filesystem::copy_file(cachedExecutablePath, linker.outputPath, std::filesystem::copy_options::overwrite_existing, ec);
280 } else {
281 // Link and cache the result
282 ✗ linker.run();
283 ✗ if (!cliOptions.ignoreCache)
284 ✗ cacheExecutable(objectFileCacheKeys, linker.getLinkerFlags(), additionalSourcePaths, linker.outputPath);
285 }
286 ✗ }
287
288 } // namespace spice::compiler
289