GCC Code Coverage Report


src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
175 / 181
96.7%
Branches:
29 / 36, 16 excl
80.6%
Functions:
17 / 17, 1 excl
100.0%

global/global-resource-manager.spice
Line Branch Exec Source
1 // Copyright (c) 2021-2026 ChilliBits. All rights reserved.
2
3 // Std imports
4 import "std/type/types";
5 import "std/data/vector";
6 import "std/data/unordered-map";
7 import "std/data/unordered-set";
8 import "std/data/pair";
9 import "std/time/timer";
10 import "std/io/filepath";
11 import "std/io/file";
12 import "std/os/system";
13 import "std/bindings/llvm/llvm" as llvm;
14 import "std/os/recursive-mutex";
15 import "std/math/hash";
16 import "std/text/format";
17 import "std/type/type-conversion";
18
19 // Own imports
20 import "@/ast/ast-nodes";
21 import "@/global/runtime-module-manager";
22 import "@/global/type-name-disambiguator";
23 import "@/global/type-registry";
24 import "@/exception/error-manager";
25 import "@/linker/external-linker-interface";
26 import "@/model/function" as model; // Aliased, because Function clashes with llvm::Function
27 import "@/model/interface";
28 import "@/model/struct";
29 import "@/model/union";
30 import "@/typechecker/function-manager";
31 import "@/typechecker/interface-manager";
32 import "@/typechecker/struct-manager";
33 import "@/typechecker/union-manager";
34 import "@/util/lookup-cache";
35 import "@/util/compile-progress";
36 import "@/reader/code-loc";
37 import "@/util/memory";
38 import "@/util/block-allocator";
39 import "@/util/concurrency";
40 import "@/source-file";
41 import "@/symboltablebuilder/scope";
42 import "@/driver";
43 import "@/util/test-util";
44
45 // Constants
46 public const string MAIN_FILE_NAME = "root";
47 public const string LTO_FILE_NAME = "lto-module";
48
49 /**
50 * The GlobalResourceManager is instantiated at startup of the compiler and serves as distribution point for globally used assets.
51 * This component owns all SourceFile instances and AST nodes and therefore is the resource root of the compiler.
52 * Other components of the compiler can request the required global resources from the GlobalResourceManager.
53 */
54 public type GlobalResourceManager struct {
55 1055 public string cpuName = "generic"
56 1055 public string cpuFeatures = ""
57 public llvm::LLVMContext ltoContext
58 public llvm::Module ltoModule
59 public DefaultMemoryManager memoryManager
60 public Vector<String> compileTimeStringValues
61 public BlockAllocator<ASTNode> astNodeAlloc // Used to allocate all AST nodes
62 // Used to allocate all scopes. A pool per parent scope would reserve a whole block for each scope, although most scopes have no or only a few children
63 BlockAllocator<Scope> scopeAlloc
64 RecursiveMutex scopeAllocMutex // Guards the scope pool, while the back end runs in parallel (see ConditionalLock)
65 public UnorderedMap<long, unsigned long> nodeToNodeId // Maps the address of each AST node to its node id
66 // The GlobalResourceManager owns all source files. They live in this pool (like the scopes in theirs), because
67 // a map of owning 'heap' pointers cannot be instantiated: its iterator needs a reference to a heap pointer as field type
68 BlockAllocator<SourceFile> sourceFileAlloc
69 public UnorderedMap<String, SourceFile*> sourceFiles // Maps the file paths to the source files owned by the pool above
70 public Vector<SourceFile*> sourceFilesInCreationOrder // For a deterministic order where it matters (e.g. the test main)
71 UnorderedSet<String> objectFileStems // Lower-cased object file names of all source files, see reserveObjectFileStem
72 String rootSourceFilePath // Path of the root source file, which the code locations are pretty-printed relative to
73 public Vector<Pair<Struct*, bool>> pendingStructDefaultMemberDecisions
74 public CliOptions& cliOptions
75 public ExternalLinkerInterface linker
76 public RuntimeModuleManager runtimeModuleManager
77 public TypeRegistry typeRegistry // Interns all types and hands out stable `const Type*` handles
78 public TypeNameDisambiguator typeNameDisambiguator // Hands out disambiguation suffixes for same-named types
79 // Wall times of the front end, middle end and back end over all source files
80 public Timer frontEndTimer
81 public Timer middleEndTimer
82 public Timer backEndTimer
83 public ErrorManager errorManager
84 public CompileProgress compileProgress // Progress bar on the console, if enabled (see compileProject)
85 // Lookup caches of the managers
86 public LookupCache<model::Function> functionLookupCache
87 public LookupCache<Struct> structLookupCache
88 public LookupCache<Interface> interfaceLookupCache
89 public LookupCache<Union> unionLookupCache
90 // Operator functions that were not available for certain operand types
91 public UnorderedMap</*cacheKey=*/Hash, /*registryVersion=*/unsigned long> unavailableOpFctCache
92 public RecursiveMutex symbolRegistryMutex // Guards the symbol lookup machinery during the parallel back end
93 1055 public bool abortCompilation = false
94 1055 unsigned long nextCustomTypeId = BYTE_MAX_VALUE + 1l // Start at 256 because all primitive types come first
95 1055 unsigned long nextScopeId = 0l
96 }
97
98 1055 public p GlobalResourceManager.ctor(CliOptions& cliOptions) {
99 1055 this.cliOptions = cliOptions;
100 1055 this.linker = ExternalLinkerInterface(cliOptions);
101 1055 this.runtimeModuleManager = RuntimeModuleManager(*this);
102 1055 this.astNodeAlloc = BlockAllocator<ASTNode>(this.memoryManager);
103 1055 this.scopeAlloc = BlockAllocator<Scope>(this.memoryManager);
104 1055 this.sourceFileAlloc = BlockAllocator<SourceFile>(this.memoryManager, sizeof<SourceFile>() * 4l);
105 // Context-free type creation (e.g. QualType(TY_INT)) interns into the type registry of the resource manager
106 1055 setActiveTypeRegistry(&this.typeRegistry);
107 // The name disambiguation of types operates on the type name disambiguator of the resource manager
108 1055 setActiveTypeNameDisambiguator(&this.typeNameDisambiguator);
109 // Hard errors are recorded in the error manager of the resource manager
110 1055 setActiveErrorManager(&this.errorManager);
111 // The lookups of the managers operate on the lookup caches of the resource manager. They are cleared in the destructor
112 1055 setActiveFunctionLookupCache(&this.functionLookupCache);
113 1055 setActiveStructLookupCache(&this.structLookupCache);
114 1055 setActiveInterfaceLookupCache(&this.interfaceLookupCache);
115 1055 setActiveUnionLookupCache(&this.unionLookupCache);
116 // The lookups of the managers lock the symbol registry mutex of the resource manager, while the back end runs in parallel
117 1055 setActiveSymbolRegistryMutex(&this.symbolRegistryMutex);
118
119 // Initialize the required LLVM targets
120
2/2
✓ Branch 0 taken 1038 times.
✓ Branch 1 taken 17 times.
1055 if cliOptions.isNativeTarget {
121 1038 llvm::initializeNativeTarget();
122 1038 llvm::initializeNativeAsmPrinter();
123 1038 llvm::initializeNativeAsmParser();
124 1038 } else {
125 17 llvm::initializeAllTargetInfos(); // Unlike the C++ variant, the C variant of initializeAllTargets does not do this
126 17 llvm::initializeAllTargets();
127 17 llvm::initializeAllTargetMCs();
128 17 llvm::initializeAllAsmPrinters();
129 17 llvm::initializeAllAsmParsers();
130 17 }
131
132 // Create target machine for LLVM
133
1/2
✓ Branch 0 taken 1055 times.
✗ Branch 1 not taken.
1055 if cliOptions.isNativeTarget & cliOptions.useNativeFeatures {
134 // Retrieve native CPU name and the supported CPU features
135 ✗ this.cpuName = llvm::getHostCPUName();
136 ✗ this.cpuFeatures = llvm::getHostCPUFeatures();
137 ✗ }
138
139 // Create lto module
140
2/2
✓ Branch 0 taken 1053 times.
✓ Branch 1 taken 2 times.
1055 if cliOptions.useLTO {
141 // Discard value names if not required
142 2 this.ltoContext.setDiscardValueNames(!cliOptions.namesForIRValues);
143 // Create lto module
144 2 this.ltoModule = llvm::Module(LTO_FILE_NAME, this.ltoContext);
145 2 }
146 1055 }
147
148 766 public p GlobalResourceManager.dtor() {
149 // Destruct all AST nodes as their concrete node types. The node pool could only destruct them as ASTNode
150 766 Vector<ASTNode*> astNodes = this.astNodeAlloc.releaseObjects();
151
2/2
✓ Branch 0 taken 2142680 times.
✓ Branch 1 taken 766 times.
2143446 foreach ASTNode* astNode : astNodes {
152 2142680 destructASTNode(astNode);
153 2142680 }
154
155 // Notify all global components to prepare to destroy
156 766 this.typeRegistry.clear();
157 766 this.typeNameDisambiguator.clear();
158 766 this.functionLookupCache.cleanup();
159 766 this.structLookupCache.cleanup();
160 766 this.interfaceLookupCache.cleanup();
161 766 this.unionLookupCache.cleanup();
162 766 this.unavailableOpFctCache.clear();
163 // Cleanup all LLVM statics
164 766 llvm::llvm_shutdown();
165 766 }
166
167 34740 public f<CliOptions&> GlobalResourceManager.getCliOptions() {
168 34740 return this.cliOptions;
169 }
170
171 2870 public f<const String&> GlobalResourceManager.getCompileTimeStringValue(unsigned long offset) {
172 2870 return this.compileTimeStringValues.get(offset);
173 }
174
175 493 public p GlobalResourceManager.addLinkerFlag(const String& flag) {
176 493 this.linker.addLinkerFlag(flag);
177 493 }
178
179 7 public p GlobalResourceManager.addAdditionalSourcePath(const FilePath& path) {
180 7 this.linker.addAdditionalSourcePath(path);
181 7 }
182
183 148183 public f<ErrorManager&> GlobalResourceManager.getErrorManager() {
184 148183 return this.errorManager;
185 }
186
187 5057 public f<SourceFile*> GlobalResourceManager.createSourceFile(SourceFile* parent, const String& depName, const FilePath& path, bool isStdFile) {
188 5057 const String filePathStr = String(path.toString());
189
190 // Register the path of the root source file, which the code locations are pretty-printed relative to
191
4/4
✓ Branch 0 taken 4002 times.
✓ Branch 1 taken 1055 times.
✓ Branch 2 taken 4003 times.
✓ Branch 3 taken 1054 times.
5057 if parent == nil<SourceFile*> && this.rootSourceFilePath.isEmpty() {
192 1054 this.rootSourceFilePath = filePathStr;
193 1054 setRootSourceFilePath(this.rootSourceFilePath.getRaw());
194 1054 }
195
196 // Key the source files by the canonical path. Otherwise, the same
197 // file is loaded twice if two callers spell its path differently, e.g. with mixed path separators on Windows (the
198 // import collector vs. the runtime module manager), and the linker sees its symbols twice
199 5057 FilePath keyPath = path.canonical();
200
2/2
✓ Branch 0 taken 5056 times.
✓ Branch 1 taken 1 time.
5057 if keyPath.isEmpty() {
201 1 keyPath = path.lexicallyNormal();
202 1 }
203 5057 const String sourceFileKey = String(keyPath.toGenericString());
204
205 // Create the new source file if it was not added yet (e.g. by another source file that imports it)
206
2/2
✓ Branch 0 taken 1086 times.
✓ Branch 1 taken 3971 times.
5057 if !this.sourceFiles.contains(sourceFileKey) {
207 // Reserve stable storage for the source file in the pool and construct it in place
208 3971 SourceFile* newSourceFile = this.sourceFileAlloc.reserve<SourceFile>();
209 unsafe {
210 3971 __placement_new<SourceFile>(newSourceFile, *this, parent, depName, path, isStdFile);
211 }
212 3971 this.sourceFiles.upsert(sourceFileKey, newSourceFile);
213 3971 this.sourceFilesInCreationOrder.pushBack(newSourceFile);
214 3971 this.compileProgress.addSourceFile();
215 3971 }
216
217 10114 return this.sourceFiles.get(sourceFileKey);
218 5057 }
219
220 /**
221 * Create a scope in the scope pool. The pool owns all scopes and destructs them together with the resource manager.
222 *
223 * @param parent Parent scope, or nil for a global scope
224 * @param sourceFile Source file the scope belongs to
225 * @param scopeType Type of the scope
226 * @param codeLoc Code location of the scope
227 * @return New scope
228 */
229 271440 public f<Scope*> GlobalResourceManager.createScope(Scope* parent, SourceFile* sourceFile, ScopeType scopeType, const CodeLoc* codeLoc) {
230 // The back end creates scopes as well (e.g. for the test main), while it may run in parallel
231 271440 ConditionalLock _ = ConditionalLock(&this.scopeAllocMutex);
232 // Reserve stable storage for the scope in the pool and construct it in place (no Scope copy)
233 271440 Scope* scope = this.scopeAlloc.reserve<Scope>();
234 271440 __placement_new<Scope>(scope, parent, sourceFile, scopeType, codeLoc);
235 542880 return scope;
236 271440 }
237
238 /**
239 * Reserve the name of the object file of a source file (without extension), that no other source file of the compilation
240 * uses. All object files go to the same output dir, so source files with the same name in different dirs would otherwise
241 * overwrite each other's object file. The first source file keeps its plain name, the following ones get a numeric suffix.
242 * The names are compared case-insensitively, since the file systems of Windows and macOS are case-insensitive by default.
243 *
244 * @param sourceFilePath Path of the source file
245 * @return Unique object file name without extension
246 */
247 3977 public f<String> GlobalResourceManager.reserveObjectFileStem(const FilePath& sourceFilePath) {
248 3977 FilePath fileName = FilePath(sourceFilePath.getFileName());
249 3977 const String baseName = fileName.getBaseName();
250 3977 String stem = baseName;
251 3977 long suffix = 1l;
252
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 3977 times.
3988 while this.objectFileStems.contains(toLower(stem)) {
253 11 suffix++;
254 11 stem = baseName + "-" + toString(suffix);
255 11 }
256 3977 this.objectFileStems.insert(toLower(stem));
257 7954 return stem;
258 3977 }
259
260 4211 public f<unsigned long> GlobalResourceManager.getNextCustomTypeId() {
261 4211 return this.nextCustomTypeId++;
262 }
263
264 /**
265 * Determine how many source files may be compiled at the same time, based on the --jobs/-j cli option
266 *
267 * @return Number of compile jobs (always >= 1)
268 */
269 760 public f<unsigned long> GlobalResourceManager.getCompileJobCount() {
270 // An explicit job count always wins
271
1/2
✓ Branch 0 taken 760 times.
✗ Branch 1 not taken.
760 if this.cliOptions.compileJobCount > 0u {
272 760 return cast<unsigned long>(this.cliOptions.compileJobCount);
273 }
274
275 // Otherwise derive the job count from the machine. If it cannot be determined, fall back to a serial back end.
276 ✗ const long coreCount = getCPUCoreCount();
277
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ return coreCount > 0l ? cast<unsigned long>(coreCount) : 1ul;
278 }
279
280 271440 public f<unsigned long> GlobalResourceManager.getNextScopeId() {
281 271440 return this.nextScopeId++;
282 }
283
284 /**
285 * Get the id of the given AST node. Node ids are assigned in creation order per source file, so unlike the node address,
286 * they are the same in every compiler run
287 *
288 * @param node AST node
289 * @return Node id / 0 if the node was not created by the parser
290 */
291 35352 public f<unsigned long> GlobalResourceManager.getNodeId(const ASTNode* node) {
292 35352 const long nodeAddress = getNodeAddress(node);
293
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 35352 times.
35352 if !this.nodeToNodeId.contains(nodeAddress) {
294 ✗ return 0l;
295 }
296 35352 return this.nodeToNodeId.get(nodeAddress);
297 }
298
299 2 public f<unsigned long> GlobalResourceManager.getTotalLineCount() {
300 2 result = 0l;
301
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
5 foreach Pair<const String&, SourceFile*&> sourceFileEntry : this.sourceFiles {
302 3 SourceFile* sourceFile = sourceFileEntry.getSecond();
303 3 const Result<String> contentResult = readFile(sourceFile.filePath.toGenericString());
304
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if contentResult.isOk() {
305 3 const String content = contentResult.unwrap();
306 3 const unsigned long size = content.getLength();
307
2/2
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 3 times.
69 for unsigned long i = 0l; i < size; i++ {
308
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 10 times.
66 if content[i] == '\n' { result++; }
309 66 }
310 // A non-empty file without a trailing newline has an extra final line
311
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 2 times.
3 if size > 0l && content[size - cast<unsigned long>(1)] != '\n' { result++; }
312 3 }
313 3 }
314 2 }
315
316 // ─── tests ────────────────────────────────────────────────────────────────────
317
318 1 #[test]
319 f<bool> testGetTotalLineCount() {
320 1 const FilePath testDir = makeUniqueTestDir("spice-line-count-test");
321 1 const FilePath firstFile = testDir / "first.spice";
322 1 const FilePath secondFile = testDir / "second.spice";
323 1 writeTestFile(firstFile, "Line 1\nLine2\nLine3\n\nLine 5"); // No trailing newline
324 1 writeTestFile(secondFile, "Line 1\nLine 2\n"); // Trailing newline
325
326 1 unsigned long firstLineCount;
327 1 unsigned long totalLineCount;
328 {
329 // Source files look up an LLVM target, so use the native one
330 1 CliOptions cliOptions;
331 1 cliOptions.targetTriple = llvm::normalizeTargetTriple(llvm::getDefaultTargetTriple());
332 1 cliOptions.isNativeTarget = true;
333 1 GlobalResourceManager resourceManager = GlobalResourceManager(cliOptions);
334 1 const SourceFile* _first = resourceManager.createSourceFile(nil<SourceFile*>, String("first"), firstFile, false);
335 1 firstLineCount = resourceManager.getTotalLineCount();
336 1 const SourceFile* _second = resourceManager.createSourceFile(nil<SourceFile*>, String("second"), secondFile, false);
337 1 totalLineCount = resourceManager.getTotalLineCount();
338 1 }
339
340 // Clean up before asserting, so that a failure does not leave the files behind
341 1 removeTestDir(testDir);
342 1 assert firstLineCount == 5l;
343 1 assert totalLineCount == 7l;
344 1 return true;
345 1 }
346
347 1 #[test]
348 f<bool> testReserveObjectFileStemIsUniquePerCompilation() {
349 1 String first;
350 1 String second;
351 1 String third;
352 1 String differentCase;
353 1 String otherName;
354 1 String suffixLookalike;
355 {
356 1 CliOptions cliOptions;
357 1 cliOptions.targetTriple = llvm::normalizeTargetTriple(llvm::getDefaultTargetTriple());
358 1 cliOptions.isNativeTarget = true;
359 1 GlobalResourceManager resourceManager = GlobalResourceManager(cliOptions);
360 1 first = resourceManager.reserveObjectFileStem(FilePath(String("project/shared.spice")));
361 1 second = resourceManager.reserveObjectFileStem(FilePath(String("project/sub/shared.spice")));
362 1 third = resourceManager.reserveObjectFileStem(FilePath(String("project/other/shared.spice")));
363 1 differentCase = resourceManager.reserveObjectFileStem(FilePath(String("project/upper/SHARED.spice")));
364 1 otherName = resourceManager.reserveObjectFileStem(FilePath(String("project/main.spice")));
365 // A source file, whose name looks like an already reserved name with suffix, gets a suffix itself
366 1 suffixLookalike = resourceManager.reserveObjectFileStem(FilePath(String("project/shared-2.spice")));
367 1 }
368 1 assert first == "shared";
369 1 assert second == "shared-2";
370 1 assert third == "shared-3";
371 1 assert differentCase == "SHARED-4";
372 1 assert otherName == "main";
373 1 assert suffixLookalike == "shared-2-2";
374 1 return true;
375 1 }
376