GCC Code Coverage Report


src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
724 / 782
92.6%
Branches:
133 / 172, 372 excl
77.3%
Functions:
59 / 61
96.7%

driver.spice
Line Branch Exec Source
1 // Copyright (c) 2021-2026 ChilliBits. All rights reserved.
2
3 // Std imports
4 import "std/data/map";
5 import "std/text/print";
6 import "std/text/format";
7 import "std/os/env";
8 import "std/os/os";
9 import "std/os/filesystem";
10 import "std/io/cli-parser";
11 import "std/io/cli-subcommand";
12 import "std/io/cli-option";
13 import "std/io/dir";
14 import "std/io/file";
15 import "std/io/filepath";
16 import "std/text/string-ext";
17 import "std/time/time";
18 import "std/type/type-conversion";
19 import "std/bindings/llvm/llvm" as llvm;
20 import "std/bindings/tpde/tpde" as tpde;
21
22 // Own imports
23 import "@/exception/fatal-error";
24 import "util/common-util";
25 import "@/exception/cli-error";
26 import "@/util/compiler-warning";
27 import "@/util/system-util";
28
29 // Constants
30 public const string TARGET_UNKNOWN = "unknown";
31 public const string ENV_VAR_DOCKERIZED = "SPICE_DOCKERIZED";
32
33 public type OptLevel enum {
34 O0 = 0, // No optimization
35 O1 = 1, // Only basic optimizations
36 O2 = 2, // Default optimization level
37 O3 = 3, // Aggressively optimize for performance
38 Os = 4, // Optimize for code size
39 Oz = 5 // Aggressively optimize for code size
40 }
41
42 public type DebugInfoLevel enum {
43 NONE = 0, // Do not emit any debug info
44 LINE_ONLY = 1, // Only emit DWARF line tables, no type, variable or scope info
45 FULL = 2 // Emit full debug info, incl. types, variables and lexical scopes
46 }
47 const string DEBUG_INFO_NONE = "none";
48 const string DEBUG_INFO_LINE_ONLY = "line-only";
49 const string DEBUG_INFO_FULL = "full";
50
51 public type Sanitizer enum {
52 NONE = 0, // No sanitizer
53 ADDRESS = 1, // Sanitize memory accesses
54 THREAD = 2, // Sanitize threads for data race prevention
55 MEMORY = 3, // Sanitize against uninitialized memory reads
56 TYPE = 4 // Sanitize type casts and accesses
57 }
58 const string SANITIZER_NONE = "none";
59 const string SANITIZER_ADDRESS = "address";
60 const string SANITIZER_THREAD = "thread";
61 const string SANITIZER_MEMORY = "memory";
62 const string SANITIZER_TYPE = "type";
63
64 public type BuildMode enum {
65 DEBUG = 0, // Default build mode, uses -O0 per default
66 RELEASE = 1, // Build without debug information and with -O2 per default
67 TEST = 2 // Build with test main function and always emit assertions
68 }
69 const string BUILD_MODE_DEBUG = "debug";
70 const string BUILD_MODE_RELEASE = "release";
71 const string BUILD_MODE_TEST = "test";
72
73 public type OutputContainer enum {
74 EXECUTABLE = 0, // On Linux and macOS no extension, on Windows .exe files
75 OBJECT_FILE = 1, // On Linux and macOS .o files and on Windows .obj files
76 STATIC_LIBRARY = 2, // On Linux and macOS .a files and on Windows .lib files
77 SHARED_LIBRARY = 3 // On Linux .so, on macOS .dylib and on Windows .dll files
78 }
79 const string OUTPUT_CONTAINER_EXECUTABLE = "exec";
80 const string OUTPUT_CONTAINER_OBJECT_FILE = "obj";
81 const string OUTPUT_CONTAINER_STATIC_LIBRARY = "lib";
82 const string OUTPUT_CONTAINER_SHARED_LIBRARY = "dylib";
83
84 public type Backend enum {
85 LLVM = 0, // Standard LLVM CodeGen backend (default)
86 TPDE = 1 // Experimental TPDE backend - fast, -O0-quality codegen
87 }
88 const string BACKEND_LLVM = "llvm";
89 const string BACKEND_TPDE = "tpde";
90
91 1109 public type DumpSettings struct {
92 1109 public bool dumpAST = false
93 1109 public bool dumpSymbolTable = false
94 1109 public bool dumpTypes = false
95 1109 public bool dumpCacheStats = false
96 1109 public bool dumpDependencyGraph = false
97 1109 public bool dumpIR = false
98 1109 public bool dumpAssembly = false
99 1109 public bool dumpObjectFiles = false
100 1109 public bool dumpToFiles = false
101 1109 public bool abortAfterDump = false
102 }
103
104 1109 public type InstrumentationSettings struct {
105 1109 public DebugInfoLevel debugInfoLevel = DebugInfoLevel::NONE
106 1109 public bool codeCoverage = false
107 1109 public Sanitizer sanitizer = Sanitizer::NONE
108 }
109
110 1233314 public const f<bool> InstrumentationSettings.emitsDebugInfo() {
111 1233314 return this.debugInfoLevel != DebugInfoLevel::NONE;
112 }
113
114 225071 public const f<bool> InstrumentationSettings.emitsFullDebugInfo() {
115 225071 return this.debugInfoLevel == DebugInfoLevel::FULL;
116 }
117
118 /**
119 * Representation of the various cli options
120 */
121 811 public type CliOptions struct {
122 public FilePath mainSourceFile // e.g. main.spice
123 public llvm::Triple targetTriple // In format: <arch><sub>-<vendor>-<sys>-<abi>
124 public String targetArch // TARGET_UNKNOWN per default, see the constructor
125 public String targetVendor // TARGET_UNKNOWN per default, see the constructor
126 public String targetOs // TARGET_UNKNOWN per default, see the constructor
127 1109 public bool isNativeTarget = true
128 1109 public bool useNativeFeatures = false
129 1109 public bool execute = false
130 public FilePath outputDir // Where the object files go. Should always be a temp directory. "./" per default, see the constructor
131 public FilePath outputPath // Where the output binary goes.
132 1109 public BuildMode buildMode = BuildMode::DEBUG // Default build mode is debug
133 1109 public OutputContainer outputContainer = OutputContainer::EXECUTABLE // Default output container is executable
134 1109 public unsigned int compileJobCount = 0 // 0 for auto
135 public String llvmArgs
136 1109 public bool printDebugOutput = false
137 1109 public bool quiet = false
138 public DumpSettings dump
139 1109 public bool namesForIRValues = false
140 1109 public bool useLifetimeMarkers = false
141 1109 public bool useTBAAMetadata = false
142 1109 public OptLevel optLevel = OptLevel::O0 // The default optimization level for debug build mode is O0
143 1109 public bool useLTO = false
144 1109 public Backend backend = Backend::LLVM // Codegen backend selection (TPDE is experimental)
145 1109 public bool noEntryFct = false
146 1109 public bool generateTestMain = false
147 1109 public bool staticLinking = false
148 public InstrumentationSettings instrumentation
149 1109 public bool disableVerifier = !__get_build_var<bool>("spice.is_debug")
150 1109 public bool testMode = false
151 1109 public bool comparableOutput = false
152 public Map<String, String> buildVars
153 1109 public bool lintOnly = false
154 1109 public bool keepFramePointers = false
155 1109 public bool noRedZone = false
156 1109 public bool stripSymbols = false
157 }
158
159 /**
160 * The compiler does not apply field defaults, which call a constructor, e.g. 'String(TARGET_UNKNOWN)'. So they are set here.
161 */
162 1109 public p CliOptions.ctor() {
163 1109 this.targetArch = String(TARGET_UNKNOWN);
164 1109 this.targetVendor = String(TARGET_UNKNOWN);
165 1109 this.targetOs = String(TARGET_UNKNOWN);
166 1109 this.outputDir = FilePath("./");
167 1109 }
168
169 /**
170 * Set a build variable. An already existing value of the build variable is overwritten.
171 *
172 * @param key Name of the build variable
173 * @param value New value of the build variable
174 */
175 7649 public p CliOptions.setBuildVar(const String& key, const String& value) {
176 7649 this.buildVars.upsert(key, value);
177 7649 }
178
179 /**
180 * Whether a panic or failed assertion prints the stack trace of the failing call. It needs the std's libbacktrace, which is
181 * built for the host machine only, so cross-compiling leaves it out rather than fail to link. Comparable output leaves it out too,
182 * as the frames and their offsets differ between platforms and compilers.
183 *
184 * @return Prints a stack trace or not
185 */
186 15673 public const f<bool> CliOptions.printsStackTraceOnAbort() {
187
2/2
✓ Branch 0 taken 365 times.
✓ Branch 1 taken 15308 times.
15673 return this.isNativeTarget && !this.comparableOutput;
188 }
189
190 /**
191 * Abort with a cli error
192 *
193 * @param errorType Type of the error
194 * @param message Error message suffix
195 */
196 9 p raiseCliError(const CliErrorType errorType, const string message) {
197 9 const CliError cliError = CliError(errorType, message);
198 9 abortWithError(cliError.getMessage());
199 9 }
200
201 /**
202 * Helper class to setup the cli interface and command line parser
203 */
204 801 public type Driver struct {
205 public CliOptions cliOptions
206 1099 public bool shouldCompile = false
207 1099 public bool shouldInstall = false
208 1099 public bool shouldUninstall = false
209 1099 public bool shouldExecute = false
210 1099 public bool dryRun = false // For unit testing purposes
211 CliParser cliParser
212 // Whether the user named a debug info level themselves. DebugInfoLevel::NONE is also the default, so without this
213 // an explicit '--debug-info=none' would be indistinguishable from not passing the option at all.
214 1099 bool debugInfoLevelSetExplicitly = false
215 }
216
217 1099 public p Driver.ctor() {
218 1099 this.cliParser = CliParser("Spice", "Spice Programming Language");
219 1099 this.cliParser.setFooter("(c) Marc Auberer 2021-" + toString(getCurrentYear()));
220
221 // Add version flag
222 1099 this.cliParser.setVersion(buildVersionInfo());
223
224 // Create sub-commands
225 1099 this.addBuildSubcommand();
226 1099 this.addRunSubcommand();
227 1099 this.addTestSubcommand();
228 1099 this.addLintSubcommand();
229 1099 this.addInstallSubcommand();
230 1099 this.addUninstallSubcommand();
231 1099 }
232
233 /**
234 * Start the parsing process
235 *
236 * @param argc Argument count
237 * @param argv Argument vector
238 * @return Return code
239 */
240 1094 public f<int> Driver.parse(int argc, string[] argv) {
241 1094 const int exitCode = this.cliParser.parse(argc, argv);
242
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1092 times.
1094 if exitCode != EXIT_CODE_SUCCESS {
243 2 return exitCode;
244 }
245
246 // The main source file has to exist. In --test-mode, a missing file is reported by the pipeline (SourceFile.runParser)
247 // instead, so the compile path skips the check here. So do the builtin tests (dry run), which parse made-up file names.
248
6/6
✓ Branch 0 taken 1090 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1090 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 34 times.
✓ Branch 5 taken 1056 times.
1092 const bool checkCompileSourceFile = (this.shouldCompile || this.shouldInstall) && !this.dryRun && !this.cliOptions.testMode;
249
6/6
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1089 times.
✓ Branch 2 taken 1087 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 1091 times.
1092 if (checkCompileSourceFile || this.shouldUninstall) && !this.cliOptions.mainSourceFile.exists() {
250 1 printf("<main-source-file>: File does not exist: %s\n", this.cliOptions.mainSourceFile.toString());
251 1 printf("Run with --help for more information.\n");
252 1 return EXIT_CODE_ERROR;
253 }
254
255 1091 this.finalizeOptions();
256 1091 return EXIT_CODE_SUCCESS;
257 }
258
259 /**
260 * Initialize the cli options based on the input of the user
261 */
262 1091 public p Driver.enrich() {
263 // Make path of given main source file canonical and relative
264 1091 this.cliOptions.mainSourceFile.makeGeneric();
265 // --test-mode keeps the path as given
266
4/4
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 1056 times.
✓ Branch 2 taken 1088 times.
✓ Branch 3 taken 3 times.
1091 if !this.dryRun && !this.cliOptions.testMode {
267 // FilePath.relative() returns an empty path if there is no relative path (e.g. the file is on another drive than the
268 // working directory). Keep the path as it is then.
269 3 const FilePath relativeMainSourceFile = this.cliOptions.mainSourceFile.relative();
270
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if !relativeMainSourceFile.isEmpty() {
271
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 this.cliOptions.mainSourceFile = relativeMainSourceFile;
272 3 }
273 3 }
274
275 // Propagate llvm args to llvm
276
1/2
✓ Branch 0 taken 1091 times.
✗ Branch 1 not taken.
1091 if !this.cliOptions.llvmArgs.isEmpty() {
277 ✗ const Vector<String> llvmArgs = split("llvm " + this.cliOptions.llvmArgs, ' ');
278 ✗ Vector<string> resultCStr = Vector<string>(llvmArgs.getSize());
279
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ foreach const String& arg : llvmArgs {
280 ✗ resultCStr.pushBack(arg.getRaw());
281 ✗ }
282 ✗ llvm::parseCommandLineOptions(cast<int>(resultCStr.getSize()), resultCStr);
283 ✗ }
284
285 // Propagate target information
286 1091 const llvm::Triple defaultTriple = llvm::normalizeTargetTriple(llvm::getDefaultTargetTriple());
287 1091 bool targetExplicitlySelected = true;
288
2/2
✓ Branch 0 taken 1071 times.
✓ Branch 1 taken 20 times.
1091 if this.cliOptions.targetTriple.empty() {
289
1/2
✓ Branch 0 taken 1071 times.
✗ Branch 1 not taken.
1071 if this.cliOptions.targetArch == TARGET_UNKNOWN { // We have nothing -> obtain native triplet
290 1071 this.cliOptions.targetTriple = defaultTriple;
291 1071 this.cliOptions.targetArch = defaultTriple.getArchName();
292 1071 this.cliOptions.targetVendor = defaultTriple.getVendorName();
293 1071 this.cliOptions.targetOs = defaultTriple.getOSName();
294 1071 this.cliOptions.isNativeTarget = true;
295 1071 targetExplicitlySelected = false;
296 1071 } else { // We have arch, vendor and os -> obtain triplet
297 ✗ this.cliOptions.targetTriple = llvm::Triple(this.cliOptions.targetArch, this.cliOptions.targetVendor, this.cliOptions.targetOs);
298 ✗ this.cliOptions.isNativeTarget = this.cliOptions.targetTriple == defaultTriple;
299 ✗ }
300 1071 } else { // Obtain arch, vendor and os by the triplet
301 20 const llvm::Triple triple = this.cliOptions.targetTriple.normalize();
302 20 this.cliOptions.targetArch = triple.getArchName();
303 20 this.cliOptions.targetVendor = triple.getVendorName();
304 20 this.cliOptions.targetOs = triple.getOSName();
305 20 this.cliOptions.isNativeTarget = triple == defaultTriple;
306 20 }
307
308 // In comparable-output (test) mode, an explicitly selected target is treated as a cross-compilation target, even if it
309 // matches the host triple. This makes the target info in the emitted IR deterministic, instead of being stripped as it is
310 // for a native build.
311
4/4
✓ Branch 0 taken 1071 times.
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 1073 times.
✓ Branch 3 taken 18 times.
1091 if targetExplicitlySelected && this.cliOptions.comparableOutput {
312 18 this.cliOptions.isNativeTarget = false;
313 18 }
314
315 // Always preserve IR values names when dumping IR
316
2/2
✓ Branch 0 taken 857 times.
✓ Branch 1 taken 234 times.
1091 if this.cliOptions.dump.dumpIR {
317 234 this.cliOptions.namesForIRValues = true;
318 234 }
319
320 // Enable test mode when test mode was selected
321
2/2
✓ Branch 0 taken 1089 times.
✓ Branch 1 taken 2 times.
1091 if this.cliOptions.buildMode == BuildMode::TEST {
322 2 this.cliOptions.noEntryFct = true;
323 2 this.cliOptions.generateTestMain = true;
324 2 }
325
326 1091 const Sanitizer sanitizer = this.cliOptions.instrumentation.sanitizer;
327 // Memory sanitizer is only supported on Linux
328
3/4
✓ Branch 0 taken 1081 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 1091 times.
✗ Branch 3 not taken.
1091 if !this.isTargetLinux() && sanitizer == Sanitizer::MEMORY {
329 ✗ raiseCliError(CliErrorType::FEATURE_NOT_SUPPORTED_FOR_TARGET, "Memory sanitizer is only supported for Linux targets");
330 ✗ }
331 // Some sanitizers need lifetime markers to work properly
332
4/4
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 1074 times.
✓ Branch 2 taken 1069 times.
✓ Branch 3 taken 22 times.
1091 if sanitizer == Sanitizer::ADDRESS || sanitizer == Sanitizer::MEMORY {
333 22 this.cliOptions.useLifetimeMarkers = true;
334 22 }
335 // Type sanitizer needs TBAA metadata to work properly
336
2/2
✓ Branch 0 taken 1086 times.
✓ Branch 1 taken 5 times.
1091 if sanitizer == Sanitizer::TYPE {
337 5 this.cliOptions.useTBAAMetadata = true;
338 5 }
339
340 // Code coverage instrumentation needs debug line info to map counters back to source locations
341
2/2
✓ Branch 0 taken 1087 times.
✓ Branch 1 taken 4 times.
1091 if this.cliOptions.instrumentation.codeCoverage {
342
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if !this.cliOptions.instrumentation.emitsDebugInfo() {
343 // Turning debug info off on purpose would leave the counters unmappable, so say so instead of overriding it
344
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 if this.debugInfoLevelSetExplicitly {
345 1 raiseCliError(CliErrorType::INCOMPATIBLE_OPTIONS, "Code coverage instrumentation requires debug line info");
346 1 }
347 2 this.cliOptions.instrumentation.debugInfoLevel = DebugInfoLevel::LINE_ONLY;
348 2 }
349
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
4 if this.cliOptions.useLTO {
350 1 raiseCliError(CliErrorType::INCOMPATIBLE_OPTIONS, "Code coverage instrumentation is not supported in combination with LTO");
351 1 }
352 4 }
353
354 // Infer build vars from other options
355 1089 this.cliOptions.setBuildVar(String("spice.is_debug"), String(toString(this.cliOptions.buildMode == BuildMode::DEBUG)));
356 1089 this.cliOptions.setBuildVar(String("spice.is_release"), String(toString(this.cliOptions.buildMode == BuildMode::RELEASE)));
357 1089 this.cliOptions.setBuildVar(String("spice.is_test"), String(toString(this.cliOptions.buildMode == BuildMode::TEST)));
358 1089 this.cliOptions.setBuildVar(String("spice.link_static"), String(toString(this.cliOptions.staticLinking)));
359 1089 this.cliOptions.setBuildVar(String("spice.is_native_target"), String(toString(this.cliOptions.isNativeTarget)));
360 1089 this.cliOptions.setBuildVar(String("spice.target_triple"), String(this.cliOptions.targetTriple.value));
361
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1084 times.
1089 this.cliOptions.setBuildVar(String("spice.backend"), String(this.cliOptions.backend == Backend::TPDE ? BACKEND_TPDE : BACKEND_LLVM));
362
363 // Prevent incompatible option combinations
364
4/4
✓ Branch 0 taken 1086 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1088 times.
✓ Branch 3 taken 1 time.
1089 if this.cliOptions.staticLinking && this.cliOptions.outputContainer == OutputContainer::SHARED_LIBRARY {
365 1 raiseCliError(CliErrorType::INCOMPATIBLE_OPTIONS, "Cannot link statically if compiling shared library");
366 1 }
367
368 // Guards for the experimental TPDE backend - ELF only, x86_64/aarch64 only, no LTO
369
2/2
✓ Branch 0 taken 1085 times.
✓ Branch 1 taken 4 times.
1089 if this.cliOptions.backend == Backend::TPDE {
370
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if this.cliOptions.useLTO {
371 ✗ raiseCliError(CliErrorType::INCOMPATIBLE_OPTIONS, "The TPDE backend does not support LTO");
372 ✗ }
373
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if this.cliOptions.instrumentation.codeCoverage {
374 ✗ raiseCliError(CliErrorType::INCOMPATIBLE_OPTIONS, "The TPDE backend does not support code coverage instrumentation");
375 ✗ }
376
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if !this.isTargetLinux() {
377 ✗ raiseCliError(CliErrorType::FEATURE_NOT_SUPPORTED_FOR_TARGET, "The TPDE backend only supports ELF targets (Linux)");
378 ✗ }
379 4 const String archName = this.cliOptions.targetTriple.getArchTypeName();
380
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 if archName != "x86_64" && archName != "aarch64" {
381 ✗ raiseCliError(CliErrorType::FEATURE_NOT_SUPPORTED_FOR_TARGET, "The TPDE backend only supports x86_64 and aarch64");
382 ✗ }
383 // TPDE always uses the red zone in x86_64 leaf functions, regardless of the 'noredzone' function attribute
384
4/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1 time.
4 if this.cliOptions.noRedZone && archName == "x86_64" {
385 1 raiseCliError(CliErrorType::INCOMPATIBLE_OPTIONS, "The TPDE backend does not support --no-red-zone on x86_64");
386 1 }
387
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if this.cliOptions.optLevel != OptLevel::O0 {
388 ✗ print("\033[33mWarning: the TPDE backend does not optimize; -O flag will be ignored\033[0m\n");
389 ✗ }
390 4 }
391 1089 }
392
393 /**
394 * Executes the built executable
395 */
396 ✗ public p Driver.runBinary() {
397 // Print status message
398
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if this.cliOptions.printDebugOutput {
399 ✗ print("Running executable ...\n\n");
400 ✗ }
401
402 // Run executable, inheriting our standard streams so its output goes straight to the terminal
403 ✗ FilePath executablePath = this.cliOptions.outputPath;
404 ✗ executablePath.makeNative();
405 ✗ const int exitCode = run(String(executablePath.toString()));
406
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if exitCode != EXIT_CODE_SUCCESS {
407 ✗ const String message = "Your Spice executable exited with non-zero exit code " + toString(exitCode);
408 ✗ raiseCliError(CliErrorType::NON_ZERO_EXIT_CODE, message.getRaw());
409 ✗ }
410 ✗ }
411
412 /**
413 * Create the given directory, including all missing parent directories. A failure is fatal
414 *
415 * @param path Path of the directory to create
416 */
417 1090 p createDirectories(const FilePath& path) {
418
1/2
✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
1090 if mkDirs(path.toString(), MODE_ALL_RWX) != 0 {
419 ✗ abortWithError(String("Could not create directory '") + path.toString() + "'");
420 ✗ }
421 1090 }
422
423 /**
424 * Complete the cli options, once all arguments were parsed. Here, the paths are derived and the install and uninstall
425 * requests are handled.
426 */
427 1091 p Driver.finalizeOptions() {
428
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1090 times.
✓ Branch 2 taken 1089 times.
✓ Branch 3 taken 2 times.
1091 if this.shouldInstall || this.shouldUninstall {
429 // Prepare the installation path
430 2 const FilePath binDir = getSpiceBinDir();
431 2 FilePath installPath = binDir;
432 2 installPath /= this.cliOptions.mainSourceFile.getBaseName();
433
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if !this.dryRun {
434 ✗ createDirectories(binDir);
435 ✗ }
436 2 assert this.cliOptions.outputContainer == OutputContainer::EXECUTABLE;
437 2 installPath.replaceExtension(getOutputFileExtension(this.cliOptions, OutputContainer::EXECUTABLE));
438
439 // If the binary should be installed, set the output path to the Spice bin directory
440
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 if this.shouldInstall {
441
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 this.cliOptions.outputPath = installPath;
442 1 }
443
444 // If the binary should be uninstalled, check if the executable exists and uninstall it
445
3/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if this.shouldUninstall && !this.dryRun {
446
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if installPath.exists() && deleteFile(installPath.toString()) {
447 ✗ println("Successfully uninstalled.");
448 ✗ } else {
449 ✗ CompilerWarning warning = CompilerWarning(CompilerWarningType::UNINSTALL_FAILED, "The executable was not found at the expected location");
450 ✗ warning.print();
451 ✗ }
452 ✗ }
453 2 }
454
455 // Abort here if we do not need to compile
456
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1090 times.
1091 if !this.shouldCompile {
457 1 return;
458 }
459
460 // Set output path and dir
461
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1086 times.
1090 if this.shouldExecute {
462 4 this.cliOptions.execute = true;
463 4 const long millis = getCurrentMillis();
464 4 this.cliOptions.outputDir = FilePath(getTempDir()) / "spice" / "output" / toString(millis);
465 4 this.cliOptions.outputPath = this.cliOptions.outputDir / this.cliOptions.mainSourceFile.getFileName();
466
2/2
✓ Branch 0 taken 1054 times.
✓ Branch 1 taken 32 times.
1090 } else if !this.cliOptions.outputPath.isEmpty() {
467
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1054 times.
1054 if dirExists(this.cliOptions.outputPath.toString()) {
468
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ this.cliOptions.outputDir = this.cliOptions.outputPath;
469 ✗ this.cliOptions.outputPath = this.cliOptions.outputDir / this.cliOptions.mainSourceFile.getFileName();
470 ✗ } else {
471 1054 const String parentDir = this.cliOptions.outputPath.getParentDir();
472
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1054 times.
1054 if !parentDir.isEmpty() {
473 1054 this.cliOptions.outputDir = FilePath(parentDir);
474 1054 }
475 1054 }
476 1054 } else {
477 32 this.cliOptions.outputDir = FilePath("./");
478 32 this.cliOptions.outputPath = this.cliOptions.outputDir / this.cliOptions.mainSourceFile.getFileName();
479 1086 }
480
481 // Set output file extension
482 1090 this.cliOptions.outputPath.replaceExtension(getOutputFileExtension(this.cliOptions, this.cliOptions.outputContainer));
483
484 // Create the output directory in case it does not exist yet
485 1090 createDirectories(this.cliOptions.outputDir);
486 1090 }
487
488 /**
489 * Add build subcommand to cli interface
490 */
491 1099 p Driver.addBuildSubcommand() {
492 // Create sub-command itself
493 1099 CliSubcommand& subCmd = this.cliParser.addSubcommand("build", "Builds your Spice program and emits an executable");
494 1099 subCmd.addAlias("b");
495 2179 subCmd.setCallback(p() {
496 1080 this.shouldCompile = true;
497 1080 });
498
499 1099 this.addCompileSubcommandOptions(subCmd);
500 1099 this.addInstrumentationOptions(subCmd);
501
502 1406 const p(const string&) outputContainerCallback = p(const string& outputContainer) {
503 307 const String inputString = toLower(String(outputContainer));
504
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 304 times.
307 if inputString == OUTPUT_CONTAINER_EXECUTABLE {
505 3 this.cliOptions.outputContainer = OutputContainer::EXECUTABLE;
506
2/2
✓ Branch 0 taken 299 times.
✓ Branch 1 taken 5 times.
307 } else if inputString == OUTPUT_CONTAINER_OBJECT_FILE {
507 299 this.cliOptions.outputContainer = OutputContainer::OBJECT_FILE;
508
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
304 } else if inputString == OUTPUT_CONTAINER_STATIC_LIBRARY {
509 2 this.cliOptions.outputContainer = OutputContainer::STATIC_LIBRARY;
510
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
5 } else if inputString == OUTPUT_CONTAINER_SHARED_LIBRARY {
511 2 this.cliOptions.outputContainer = OutputContainer::SHARED_LIBRARY;
512 2 } else {
513 1 raiseCliError(CliErrorType::INVALID_OUTPUT_CONTAINER, inputString.getRaw());
514 304 }
515 307 };
516
517 // --output-container
518 1099 subCmd.addOption("--output-container", outputContainerCallback, "Format of the compilation output container: exec (default), obj, lib, dylib)");
519 // --target-triple
520 // A callback creates the triple, because a triple owns its message, which must not be a pointer into the arguments
521 1119 const p(const string&) targetCallback = p(const string& target) {
522 20 this.cliOptions.targetTriple = llvm::Triple(target);
523 20 };
524 1099 CliOption<string>& targetOption = subCmd.addOption("--target", targetCallback, "Target triple for the emitted executable (for cross-compiling)");
525 1099 targetOption.addAlias("--target-triple");
526 1099 targetOption.addAlias("-t");
527 // --target-arch
528 1099 subCmd.addOption("--target-arch", this.cliOptions.targetArch, "Target arch for emitted executable (for cross-compiling)");
529 // --target-vendor
530 1099 subCmd.addOption("--target-vendor", this.cliOptions.targetVendor, "Target vendor for emitted executable (for cross-compiling)");
531 // --target-os
532 1099 subCmd.addOption("--target-os", this.cliOptions.targetOs, "Target os for emitted executable (for cross-compiling)");
533 // --output
534 1099 CliOption<FilePath>& outputOption = subCmd.addOption("--output", this.cliOptions.outputPath, "Set the output file path");
535 1099 outputOption.addAlias("-o");
536 // --disable-verifier
537 1099 subCmd.addFlag("--disable-verifier", this.cliOptions.disableVerifier, "Disable LLVM module and function verification");
538 // --no-entry
539 1099 subCmd.addFlag("--no-entry", this.cliOptions.noEntryFct, "Do not generate main function");
540 // --test-main
541 // The test runner builds the test cases, which run their enclosed tests, in debug mode. So it needs the test main function
542 // without switching to the test build mode, like the test subcommand would do
543 1109 const p(const bool&) testMainCallback = p(const bool& _v) {
544 10 this.cliOptions.generateTestMain = true; // An alternative entry function is generated
545 10 this.cliOptions.noEntryFct = true; // To not have two main functions, disable normal main
546 10 };
547 1099 subCmd.addFlag("--test-main", testMainCallback, "Generate a main function, that runs all enclosed tests, instead of the normal one. For internal use");
548 // --static
549 1099 subCmd.addFlag("--static", this.cliOptions.staticLinking, "Link statically");
550 // --strip-symbols
551 1099 subCmd.addFlag("--strip-symbols", this.cliOptions.stripSymbols, "Strip the symbol table from the executable, making it smaller at the price of unreadable stack traces and, together with --debug-info, no debug info either");
552 // --dump-to-files
553 1099 subCmd.addFlag("--dump-to-files", this.cliOptions.dump.dumpToFiles, "Redirect dumps to files instead of printing");
554 // --abort-after-dump
555 1099 subCmd.addFlag("--abort-after-dump", this.cliOptions.dump.abortAfterDump, "Abort the compilation process after dumping the first requested resource");
556 1099 }
557
558 /**
559 * Add run subcommand to cli interface
560 */
561 1099 p Driver.addRunSubcommand() {
562 // Create sub-command itself
563 1099 CliSubcommand& subCmd = this.cliParser.addSubcommand("run", "Builds your Spice program and runs it immediately");
564 1099 subCmd.addAlias("r");
565 1101 subCmd.setCallback(p() {
566 2 this.shouldCompile = true;
567 2 this.shouldExecute = true;
568 2 });
569
570 1099 this.addCompileSubcommandOptions(subCmd);
571 1099 this.addInstrumentationOptions(subCmd);
572
573 // --disable-verifier
574 1099 subCmd.addFlag("--disable-verifier", this.cliOptions.disableVerifier, "Disable LLVM module and function verification");
575 1099 }
576
577 /**
578 * Add test subcommand to cli interface
579 */
580 1099 p Driver.addTestSubcommand() {
581 // Create sub-command itself
582 1099 CliSubcommand& subCmd = this.cliParser.addSubcommand("test", "Builds your Spice program and runs all enclosed tests");
583 1099 subCmd.addAlias("t");
584 1101 subCmd.setCallback(p() {
585 2 this.shouldCompile = true;
586 2 this.shouldExecute = true;
587 2 this.cliOptions.buildMode = BuildMode::TEST;
588 2 this.cliOptions.generateTestMain = true; // An alternative entry function is generated
589 2 this.cliOptions.noEntryFct = true; // To not have two main functions, disable normal main
590 2 });
591
592 1099 this.addCompileSubcommandOptions(subCmd);
593 1099 this.addInstrumentationOptions(subCmd);
594
595 // --disable-verifier
596 1099 subCmd.addFlag("--disable-verifier", this.cliOptions.disableVerifier, "Disable LLVM module and function verification");
597 1099 }
598
599 /**
600 * Add lint subcommand to cli interface
601 */
602 1099 p Driver.addLintSubcommand() {
603 // Create sub-command itself
604 1099 CliSubcommand& subCmd = this.cliParser.addSubcommand("lint", "Checks your Spice program for style and best-practice issues");
605 1099 subCmd.addAlias("l");
606 1104 subCmd.setCallback(p() {
607 5 this.shouldCompile = true;
608 5 this.cliOptions.lintOnly = true;
609 5 });
610
611 1099 this.addCompileSubcommandOptions(subCmd);
612 1099 }
613
614 /**
615 * Add install subcommand to cli interface
616 */
617 1099 p Driver.addInstallSubcommand() {
618 // Create sub-command itself
619 1099 CliSubcommand& subCmd = this.cliParser.addSubcommand("install", "Builds your Spice program and installs it to a directory in the PATH");
620 1099 subCmd.addAlias("i");
621 1100 subCmd.setCallback(p() {
622 1 this.shouldCompile = true;
623 1 this.shouldInstall = true;
624 1 this.ensureNotDockerized();
625 1 });
626
627 1099 this.addCompileSubcommandOptions(subCmd);
628 1099 }
629
630 /**
631 * Add uninstall subcommand to cli interface
632 */
633 1099 p Driver.addUninstallSubcommand() {
634 // Create sub-command itself
635 1099 CliSubcommand& subCmd = this.cliParser.addSubcommand("uninstall", "Uninstalls a Spice program from the system");
636 1099 subCmd.addAlias("u");
637 1101 subCmd.setCallback(p() {
638 2 this.shouldUninstall = true;
639 2 this.ensureNotDockerized();
640 2 });
641
642 // Source file
643 1099 subCmd.addOption("main-source-file", this.cliOptions.mainSourceFile, "Main source file");
644 1099 }
645
646 5495 p Driver.addCompileSubcommandOptions(CliSubcommand& subCmd) {
647 5504 const p(const string&) buildModeCallback = p(const string& buildMode) {
648 9 const String inputString = toLower(String(buildMode));
649
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if inputString == BUILD_MODE_DEBUG {
650 ✗ this.cliOptions.buildMode = BuildMode::DEBUG;
651
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 time.
9 } else if inputString == BUILD_MODE_RELEASE {
652 8 this.cliOptions.buildMode = BuildMode::RELEASE;
653
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
9 } else if inputString == BUILD_MODE_TEST {
654 ✗ this.cliOptions.buildMode = BuildMode::TEST;
655 ✗ } else {
656 1 raiseCliError(CliErrorType::INVALID_BUILD_MODE, inputString.getRaw());
657 9 }
658 9 };
659 5521 const p(const string&) buildVarCallback = p(const string& buildVar) {
660 26 const String input = String(buildVar);
661
662 // Skip empty inputs
663
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if input.isEmpty() {
664 ✗ return;
665 }
666
667 // Parse structure: "key=value". If no value given, save "true" as value.
668 26 const long splitPos = input.find('=');
669
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 8 times.
26 if splitPos != -1 {
670 18 const String key = input.getSubstring(0, splitPos);
671 18 const String value = input.getSubstring(splitPos + 1);
672 18 this.cliOptions.setBuildVar(key, value);
673 18 } else {
674 8 this.cliOptions.setBuildVar(input, String("true"));
675 8 }
676 26 };
677
678 // --build-mode
679 5495 CliOption<string>& buildModeOption = subCmd.addOption("--build-mode", buildModeCallback, "Build mode: debug (default), release, test");
680 5495 buildModeOption.addAlias("-m");
681 // --llvm-args
682 5495 CliOption<String>& llvmArgsOption = subCmd.addOption("--llvm-args", this.cliOptions.llvmArgs, "Additional arguments for LLVM");
683 5495 llvmArgsOption.addAlias("-llvm");
684 // --jobs
685 5495 CliOption<int>& jobsOption = subCmd.addOption("--jobs", this.cliOptions.compileJobCount, "Number of source files to compile in parallel in the back end (0 = auto)");
686 5495 jobsOption.addAlias("-j");
687 // --build-var
688 5495 CliOption<string>& buildVarOption = subCmd.addOption("--build-var", buildVarCallback, "Add build variable to parametrize the compiled program (e.g. -v key=value)");
689 5495 buildVarOption.addAlias("-b");
690 5495 buildVarOption.allowMultipleValues();
691 // --use-lifetime-markers
692 5495 subCmd.addFlag("--use-lifetime-markers", this.cliOptions.useLifetimeMarkers, "Generate lifetime markers to enhance optimizations");
693 // --use-tbaa-metadata
694 5495 subCmd.addFlag("--use-tbaa-metadata", this.cliOptions.useTBAAMetadata, "Generate metadata for type-based alias analysis to enhance optimizations");
695 // --keep-frame-pointers
696 5495 subCmd.addFlag("--keep-frame-pointers", this.cliOptions.keepFramePointers, "Set up a frame pointer in every function, so external profilers and debuggers can walk the stack (costs a register and a bit of performance)");
697 // --no-red-zone
698 5495 subCmd.addFlag("--no-red-zone", this.cliOptions.noRedZone, "Do not use the red zone below the stack pointer (x86_64). Required for code that runs in kernel mode, where interrupts push onto the current stack");
699 // --native-features
700 5495 subCmd.addFlag("--native-features", this.cliOptions.useNativeFeatures, "Tune the output for the CPU of the compiling machine and use all of its features. The output might not run on other machines of the same architecture");
701 // --test-mode
702 // The test runner drives the compiler as separate process, so it needs an option to compile in the test mode.
703 6553 const p(const bool&) testModeCallback = p(const bool& _v) {
704 1058 this.cliOptions.testMode = true;
705 1058 this.cliOptions.comparableOutput = true;
706 1058 this.cliOptions.namesForIRValues = true;
707 // Compile serially: the test cases are already spread over processes, so a parallel back
708 // end would only oversubscribe the machine, and the reference outputs stay strictly deterministic this way
709 1058 this.cliOptions.compileJobCount = 1u;
710 1058 };
711 5495 subCmd.addFlag("--test-mode", testModeCallback, "Compile like the test runner does (test mode, comparable output). For internal use");
712
713 // Opt levels
714 5689 subCmd.addFlag("-O0", p(const bool& _v) { this.cliOptions.optLevel = OptLevel::O0; }, "Disable optimization.");
715 5496 subCmd.addFlag("-O1", p(const bool& _v) { this.cliOptions.optLevel = OptLevel::O1; }, "Only basic optimization is applied.");
716 5531 subCmd.addFlag("-O2", p(const bool& _v) { this.cliOptions.optLevel = OptLevel::O2; }, "More advanced optimization is applied.");
717 5504 subCmd.addFlag("-O3", p(const bool& _v) { this.cliOptions.optLevel = OptLevel::O3; }, "Aggressive optimization for best performance.");
718 5498 subCmd.addFlag("-Os", p(const bool& _v) { this.cliOptions.optLevel = OptLevel::Os; }, "Size optimization for output executable.");
719 5497 subCmd.addFlag("-Oz", p(const bool& _v) { this.cliOptions.optLevel = OptLevel::Oz; }, "Aggressive optimization for best size.");
720 5495 subCmd.addFlag("-lto", this.cliOptions.useLTO, "Enable link time optimization (LTO)");
721
722 // --backend
723 5503 const p(const string&) backendCallback = p(const string& backend) {
724 8 const String inputString = toLower(String(backend));
725
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
8 if inputString == BACKEND_LLVM {
726 2 this.cliOptions.backend = Backend::LLVM;
727
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
8 } else if inputString == BACKEND_TPDE {
728 // The compiler is backed by TPDE, if the TPDE libraries were linked into it via TPDE_FLAGS (see std/bindings/tpde)
729
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if !tpde::isAvailable() {
730 ✗ raiseCliError(CliErrorType::FEATURE_NOT_SUPPORTED_FOR_TARGET, "The TPDE backend is not available in this build. Rebuild the compiler with TPDE_FLAGS set.");
731 ✗ }
732 5 this.cliOptions.backend = Backend::TPDE;
733 5 } else {
734 1 raiseCliError(CliErrorType::INVALID_BACKEND, inputString.getRaw());
735 6 }
736 8 };
737 5495 subCmd.addOption("--backend", backendCallback, "Codegen backend: llvm (default), tpde (experimental — fast, unoptimized; requires opt-in build)");
738
739 // --debug-output
740 5495 CliOption<bool>& debugOutputFlag = subCmd.addFlag("--debug-output", this.cliOptions.printDebugOutput, "Enable debug output");
741 5495 debugOutputFlag.addAlias("-d");
742 // --quiet
743 5495 CliOption<bool>& quietFlag = subCmd.addFlag("--quiet", this.cliOptions.quiet, "Do not show the compile progress");
744 5495 quietFlag.addAlias("-q");
745 // --dump-ast
746 5495 CliOption<bool>& dumpAstFlag = subCmd.addFlag("--dump-ast", this.cliOptions.dump.dumpAST, "Dump AST as serialized string and SVG image");
747 5495 dumpAstFlag.addAlias("-ast");
748 // --dump-symtab
749 5495 subCmd.addFlag("--dump-symtab", this.cliOptions.dump.dumpSymbolTable, "Dump serialized symbol tables");
750 // --dump-types
751 5495 subCmd.addFlag("--dump-types", this.cliOptions.dump.dumpTypes, "Dump all used types");
752 // --dump-cache-stats
753 5495 subCmd.addFlag("--dump-cache-stats", this.cliOptions.dump.dumpCacheStats, "Dump stats for compiler-internal lookup caches");
754 // --dump-ir
755 5495 CliOption<bool>& dumpIrFlag = subCmd.addFlag("--dump-ir", this.cliOptions.dump.dumpIR, "Dump LLVM-IR");
756 5495 dumpIrFlag.addAlias("-ir");
757 // --dump-assembly
758 5495 CliOption<bool>& dumpAsmFlag = subCmd.addFlag("--dump-assembly", this.cliOptions.dump.dumpAssembly, "Dump Assembly code");
759 5495 dumpAsmFlag.addAlias("-asm");
760 5495 dumpAsmFlag.addAlias("-s");
761 // --dump-object-file
762 5495 subCmd.addFlag("--dump-object-file", this.cliOptions.dump.dumpObjectFiles, "Dump object files");
763 // --dump-dependency-graph
764 5495 subCmd.addFlag("--dump-dependency-graph", this.cliOptions.dump.dumpDependencyGraph, "Dump compile unit dependency graph");
765
766 // Source file. The parser requires positional options, and the existence of the file is checked after parsing
767 5495 subCmd.addOption("main-source-file", this.cliOptions.mainSourceFile, "Main source file");
768 5495 }
769
770 3297 p Driver.addInstrumentationOptions(CliSubcommand& subCmd) {
771 3331 const p(const string&) sanitizerCallback = p(const string& sanitizer) {
772 34 const String inputString = toLower(String(sanitizer));
773
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
34 if inputString == SANITIZER_NONE {
774 ✗ this.cliOptions.instrumentation.sanitizer = Sanitizer::NONE;
775
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 17 times.
34 } else if inputString == SANITIZER_ADDRESS {
776 17 this.cliOptions.instrumentation.sanitizer = Sanitizer::ADDRESS;
777
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 11 times.
34 } else if inputString == SANITIZER_THREAD {
778 6 this.cliOptions.instrumentation.sanitizer = Sanitizer::THREAD;
779
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 6 times.
17 } else if inputString == SANITIZER_MEMORY {
780 5 this.cliOptions.instrumentation.sanitizer = Sanitizer::MEMORY;
781
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
11 } else if inputString == SANITIZER_TYPE {
782 5 this.cliOptions.instrumentation.sanitizer = Sanitizer::TYPE;
783 5 } else {
784 1 raiseCliError(CliErrorType::INVALID_SANITIZER, inputString.getRaw());
785 34 }
786 34 };
787
788 3315 const p(const string&) debugInfoCallback = p(const string& debugInfoLevel) {
789 18 const String inputString = toLower(String(debugInfoLevel));
790 // A flag without an attached value is reported as 'true', so accept the boolean values
791
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 17 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 13 times.
18 if inputString == "true" || inputString == DEBUG_INFO_FULL {
792 5 this.setDebugInfoLevel(DebugInfoLevel::FULL);
793
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 5 times.
18 } else if inputString == DEBUG_INFO_LINE_ONLY {
794 8 this.setDebugInfoLevel(DebugInfoLevel::LINE_ONLY);
795
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 time.
13 } else if inputString == "false" || inputString == DEBUG_INFO_NONE {
796 4 this.setDebugInfoLevel(DebugInfoLevel::NONE);
797 4 } else {
798 1 raiseCliError(CliErrorType::INVALID_DEBUG_INFO_LEVEL, inputString.getRaw());
799 13 }
800 18 };
801
802 // --debug-info
803 // The cli parser has no options with an optional value. Therefore, the level can only be attached with '=', which is
804 // handled by the option. The bare '--debug-info' is a flag of its own, which keeps it from swallowing the main source
805 // file. The one that comes last on the command line wins.
806 3297 subCmd.addOption("--debug-info", debugInfoCallback, "Generate debug info: full (default), line-only, none");
807 3299 subCmd.addFlag("--debug-info", p(const bool& _v) { this.setDebugInfoLevel(DebugInfoLevel::FULL); }, "Alias for --debug-info=full");
808 // -g
809 3320 subCmd.addFlag("-g", p(const bool& _v) { this.setDebugInfoLevel(DebugInfoLevel::FULL); }, "Alias for --debug-info=full");
810 // --sanitizer
811 3297 subCmd.addOption("--sanitizer", sanitizerCallback, "Enable sanitizer: none (default), address, thread, memory, type");
812 // --coverage
813 3297 subCmd.addFlag("--coverage", this.cliOptions.instrumentation.codeCoverage, "Instrument code for coverage analysis (gcov-compatible .gcno/.gcda output)");
814 3297 }
815
816 /**
817 * Set the debug info level, which was named explicitly by the user
818 *
819 * @param level Debug info level
820 */
821 42 p Driver.setDebugInfoLevel(const DebugInfoLevel level) {
822 42 this.cliOptions.instrumentation.debugInfoLevel = level;
823 42 this.debugInfoLevelSetExplicitly = true;
824 42 }
825
826 /**
827 * Check if the target of the compilation is a Linux target
828 *
829 * @return Linux target or not
830 */
831 1096 f<bool> Driver.isTargetLinux() {
832 1096 return this.cliOptions.targetOs.startsWith("linux");
833 }
834
835 /**
836 * Ensure that the compiler is not running in a Docker container
837 */
838 3 const p Driver.ensureNotDockerized() {
839 3 const Result<string> envValue = getEnv(ENV_VAR_DOCKERIZED);
840
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if envValue.isOk() && envValue.unwrap() == "true" {
841 ✗ raiseCliError(CliErrorType::FEATURE_NOT_SUPPORTED_WHEN_DOCKERIZED, "This feature is not supported in a containerized environment. Please use the standalone version of Spice.");
842 ✗ }
843 3 }
844
845 // ─── tests ────────────────────────────────────────────────────────────────────
846
847 // Parse the given arguments and enrich the cli options of the given driver. The driver has to be created by the caller
848 // and must not be moved afterwards, because the callbacks of the cli parser refer to it.
849 35 f<bool> parseAndEnrich(Driver& driver, int argc, string[] argv) {
850 35 driver.dryRun = true;
851
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 35 times.
35 if driver.parse(argc, argv) != EXIT_CODE_SUCCESS {
852 ✗ return false;
853 }
854 35 driver.enrich();
855 35 return true;
856 }
857
858 // Parse the given arguments of the given driver without enriching the cli options. The driver has to be created by the caller
859 3 f<int> parseOnly(Driver& driver, int argc, string[] argv) {
860 3 driver.dryRun = true;
861 3 return driver.parse(argc, argv);
862 }
863
864 1 #[test]
865 f<bool> testBuildAndRunSubcommands() {
866 // Build: minimal
867 1 Driver buildMinimal;
868 1 string[3] buildMinimalArgs = ["spice", "build", "main-file.spice"];
869 1 assert parseAndEnrich(buildMinimal, 3, buildMinimalArgs);
870 1 assert buildMinimal.shouldCompile;
871 1 assert !buildMinimal.shouldInstall;
872 1 assert !buildMinimal.shouldUninstall;
873 1 assert !buildMinimal.shouldExecute;
874 1 assert !buildMinimal.cliOptions.execute;
875 1 assert buildMinimal.cliOptions.mainSourceFile.toGenericString() == "main-file.spice";
876 1 assert buildMinimal.cliOptions.optLevel == OptLevel::O0;
877 1 assert buildMinimal.cliOptions.buildMode == BuildMode::DEBUG;
878 1 assert buildMinimal.cliOptions.outputContainer == OutputContainer::EXECUTABLE;
879 1 assert buildMinimal.cliOptions.backend == Backend::LLVM;
880 1 assert !buildMinimal.cliOptions.generateTestMain;
881 1 assert !buildMinimal.cliOptions.testMode;
882 1 assert !buildMinimal.cliOptions.noEntryFct;
883 1 assert !buildMinimal.cliOptions.lintOnly;
884 1 assert !buildMinimal.cliOptions.keepFramePointers;
885 1 assert !buildMinimal.cliOptions.noRedZone;
886 1 assert !buildMinimal.cliOptions.stripSymbols;
887 1 assert !buildMinimal.cliOptions.instrumentation.emitsDebugInfo();
888 // The executable lands in the current dir and is named like the source file, with the extension of the target
889 1 assert buildMinimal.cliOptions.outputDir.toGenericString() == "./";
890 1 FilePath expectedOutputPath = FilePath("./main-file");
891 1 expectedOutputPath.replaceExtension(getOutputFileExtension(buildMinimal.cliOptions, OutputContainer::EXECUTABLE));
892 1 assert buildMinimal.cliOptions.outputPath.toGenericString() == expectedOutputPath.toGenericString();
893
894 // Build: complex
895 1 Driver buildComplex;
896 1 string[14] buildComplexArgs = ["spice", "b", "-d", "-ir", "-g", "-Os", "-m", "release", "-lto", "--sanitizer", "address", "--output-container", "exec", "main-file.spice"];
897 1 assert parseAndEnrich(buildComplex, 14, buildComplexArgs);
898 1 assert buildComplex.shouldCompile;
899 1 assert !buildComplex.shouldExecute;
900 1 assert buildComplex.cliOptions.mainSourceFile.toGenericString() == "main-file.spice";
901 1 assert buildComplex.cliOptions.optLevel == OptLevel::Os;
902 1 assert buildComplex.cliOptions.buildMode == BuildMode::RELEASE;
903 1 assert buildComplex.cliOptions.instrumentation.debugInfoLevel == DebugInfoLevel::FULL;
904 1 assert buildComplex.cliOptions.instrumentation.sanitizer == Sanitizer::ADDRESS;
905 1 assert buildComplex.cliOptions.outputContainer == OutputContainer::EXECUTABLE;
906 1 assert buildComplex.cliOptions.useLTO;
907 1 assert buildComplex.cliOptions.printDebugOutput;
908 1 assert buildComplex.cliOptions.dump.dumpIR;
909 1 assert buildComplex.cliOptions.namesForIRValues; // implicitly due to dumping the IR
910 1 assert buildComplex.cliOptions.useLifetimeMarkers; // implicitly due to the enabled address sanitizer
911
912 // Build: option values, which need the parser to consume the following argument
913 1 Driver buildOptions;
914 1 string[18] buildOptionsWithFile = ["spice", "build", "--strip-symbols", "--keep-frame-pointers", "--no-red-zone", "--no-entry", "--static", "--backend", "llvm", "-b", "key=value", "--build-var", "flag", "-j", "8", "--output-container", "lib", "main-file.spice"];
915 1 assert parseAndEnrich(buildOptions, 18, buildOptionsWithFile);
916 1 assert buildOptions.cliOptions.stripSymbols;
917 1 assert buildOptions.cliOptions.keepFramePointers;
918 1 assert buildOptions.cliOptions.noRedZone;
919 1 assert buildOptions.cliOptions.noEntryFct;
920 1 assert buildOptions.cliOptions.staticLinking;
921 1 assert buildOptions.cliOptions.compileJobCount == 8;
922 1 assert buildOptions.cliOptions.outputContainer == OutputContainer::STATIC_LIBRARY;
923 1 assert buildOptions.cliOptions.buildVars.get(String("key")) == "value";
924 1 assert buildOptions.cliOptions.buildVars.get(String("flag")) == "true"; // a build var without a value is 'true'
925 1 assert buildOptions.cliOptions.buildVars.get(String("spice.backend")) == "llvm";
926
927 // Build: test main without the test build mode, like the test runner requests it
928 1 Driver buildTestMain;
929 1 string[4] buildTestMainArgs = ["spice", "build", "--test-main", "main-file.spice"];
930 1 assert parseAndEnrich(buildTestMain, 4, buildTestMainArgs);
931 1 assert buildTestMain.cliOptions.generateTestMain;
932 1 assert buildTestMain.cliOptions.noEntryFct;
933 1 assert buildTestMain.cliOptions.buildMode == BuildMode::DEBUG;
934
935 // Run: minimal
936 1 Driver runMinimal;
937 1 string[3] runMinimalArgs = ["spice", "run", "main-file.spice"];
938 1 assert parseAndEnrich(runMinimal, 3, runMinimalArgs);
939 1 assert runMinimal.shouldCompile;
940 1 assert !runMinimal.shouldInstall;
941 1 assert !runMinimal.shouldUninstall;
942 1 assert runMinimal.shouldExecute;
943 1 assert runMinimal.cliOptions.execute;
944 1 assert runMinimal.cliOptions.optLevel == OptLevel::O0;
945 1 assert !runMinimal.cliOptions.generateTestMain;
946 // The executable is built into a temporary directory
947 1 const String runOutputDir = String(runMinimal.cliOptions.outputDir.toGenericString());
948 1 assert runOutputDir.contains("spice/output/");
949
950 // Run: complex
951 1 Driver runComplex;
952 1 string[7] runComplexArgs = ["spice", "r", "-O2", "-j", "8", "-ast", "main-file.spice"];
953 1 assert parseAndEnrich(runComplex, 7, runComplexArgs);
954 1 assert runComplex.shouldExecute;
955 1 assert runComplex.cliOptions.execute;
956 1 assert runComplex.cliOptions.optLevel == OptLevel::O2;
957 1 assert runComplex.cliOptions.compileJobCount == 8;
958 1 assert runComplex.cliOptions.dump.dumpAST;
959 1 return true;
960 1 }
961
962 1 #[test]
963 f<bool> testTestLintInstallSubcommands() {
964 // Test: minimal
965 1 Driver testMinimal;
966 1 string[3] testMinimalArgs = ["spice", "test", "main-file.spice"];
967 1 assert parseAndEnrich(testMinimal, 3, testMinimalArgs);
968 1 assert testMinimal.shouldCompile;
969 1 assert testMinimal.shouldExecute;
970 1 assert testMinimal.cliOptions.execute;
971 1 assert testMinimal.cliOptions.buildMode == BuildMode::TEST;
972 1 assert testMinimal.cliOptions.generateTestMain;
973 1 assert testMinimal.cliOptions.noEntryFct;
974 1 assert !testMinimal.cliOptions.testMode;
975
976 // Test: complex
977 1 Driver testComplex;
978 1 string[8] testComplexArgs = ["spice", "t", "-s", "--sanitizer", "thread", "--disable-verifier", "-q", "main-file.spice"];
979 1 assert parseAndEnrich(testComplex, 8, testComplexArgs);
980 1 assert testComplex.cliOptions.dump.dumpAssembly;
981 1 assert testComplex.cliOptions.instrumentation.sanitizer == Sanitizer::THREAD;
982 1 assert testComplex.cliOptions.disableVerifier;
983 1 assert testComplex.cliOptions.quiet;
984
985 // Lint
986 1 Driver lintMinimal;
987 1 string[3] lintMinimalArgs = ["spice", "lint", "main-file.spice"];
988 1 assert parseAndEnrich(lintMinimal, 3, lintMinimalArgs);
989 1 assert lintMinimal.shouldCompile;
990 1 assert !lintMinimal.shouldExecute;
991 1 assert lintMinimal.cliOptions.lintOnly;
992 1 assert lintMinimal.cliOptions.buildMode == BuildMode::DEBUG;
993
994 1 Driver lintComplex;
995 1 string[8] lintComplexArgs = ["spice", "l", "-d", "-ast", "-Os", "-m", "release", "main-file.spice"];
996 1 assert parseAndEnrich(lintComplex, 8, lintComplexArgs);
997 1 assert lintComplex.cliOptions.lintOnly;
998 1 assert lintComplex.cliOptions.optLevel == OptLevel::Os;
999 1 assert lintComplex.cliOptions.buildMode == BuildMode::RELEASE;
1000 1 assert lintComplex.cliOptions.printDebugOutput;
1001 1 assert lintComplex.cliOptions.dump.dumpAST;
1002
1003 // Install: the executable is built into the bin directory of Spice
1004 1 Driver install;
1005 1 string[3] installArgs = ["spice", "install", "main-file.spice"];
1006 1 assert parseAndEnrich(install, 3, installArgs);
1007 1 assert install.shouldCompile;
1008 1 assert install.shouldInstall;
1009 1 assert !install.shouldUninstall;
1010 1 assert !install.shouldExecute;
1011 // Compare generic strings, because the separators of a FilePath depend on which of its helpers touched it last
1012 1 FilePath binDir = getSpiceBinDir();
1013 1 const String installedPath = String(install.cliOptions.outputPath.toGenericString());
1014 1 assert installedPath.startsWith(binDir.toGenericString());
1015
1016 // Uninstall: only the source file is required and nothing gets compiled. The source file has to exist for uninstall.
1017 // Create it in the temp dir, so that no file in the working directory gets overwritten or deleted
1018 1 const FilePath uninstallSourceFilePath = FilePath(getTempDir()) / FilePath("spice-driver-uninstall-test.spice");
1019 1 const String uninstallSourceFile = String(uninstallSourceFilePath.toString());
1020 1 const Result<bool> writeResult = writeFile(uninstallSourceFile.getRaw(), "");
1021 1 assert writeResult.isOk();
1022 1 Driver uninstall;
1023 1 string[3] uninstallArgs = ["spice", "uninstall", uninstallSourceFile.getRaw()];
1024 1 const bool uninstallParsed = parseAndEnrich(uninstall, 3, uninstallArgs);
1025 1 assert deleteFile(uninstallSourceFile.getRaw());
1026 1 assert uninstallParsed;
1027 1 assert !uninstall.shouldCompile;
1028 1 assert !uninstall.shouldInstall;
1029 1 assert uninstall.shouldUninstall;
1030 1 assert !uninstall.shouldExecute;
1031 1 assert String(uninstall.cliOptions.mainSourceFile.toGenericString()) == uninstallSourceFilePath.toGenericString();
1032 1 return true;
1033 1 }
1034
1035 12 f<DebugInfoLevel> parseDebugInfoLevel(const string firstArg, const string secondArg, const string mainFile) {
1036 12 Driver driver;
1037 12 string[5] args = ["spice", "build", firstArg, secondArg, mainFile];
1038 12 assert parseAndEnrich(driver, 5, args);
1039 24 return driver.cliOptions.instrumentation.debugInfoLevel;
1040 12 }
1041
1042 1 #[test]
1043 f<bool> testInstrumentation() {
1044 // All debug info levels. A level without a value must not swallow the source file
1045 1 assert parseDebugInfoLevel("-g", "-d", "main-file.spice") == DebugInfoLevel::FULL; // Plain alias for --debug-info=full
1046 1 assert parseDebugInfoLevel("--debug-info", "-d", "main-file.spice") == DebugInfoLevel::FULL;
1047 1 assert parseDebugInfoLevel("--debug-info=full", "-d", "main-file.spice") == DebugInfoLevel::FULL;
1048 1 assert parseDebugInfoLevel("--debug-info=line-only", "-d", "main-file.spice") == DebugInfoLevel::LINE_ONLY;
1049 1 assert parseDebugInfoLevel("--debug-info=none", "-d", "main-file.spice") == DebugInfoLevel::NONE;
1050 // Levels are case-insensitive, and the boolean values are accepted
1051 1 assert parseDebugInfoLevel("--debug-info=LINE-ONLY", "-d", "main-file.spice") == DebugInfoLevel::LINE_ONLY;
1052 1 assert parseDebugInfoLevel("--debug-info=Full", "-d", "main-file.spice") == DebugInfoLevel::FULL;
1053 1 assert parseDebugInfoLevel("--debug-info=true", "-d", "main-file.spice") == DebugInfoLevel::FULL;
1054 1 assert parseDebugInfoLevel("--debug-info=false", "-d", "main-file.spice") == DebugInfoLevel::NONE;
1055
1056 // The last occurrence wins, no matter if '-g' or '--debug-info' comes last
1057 1 assert parseDebugInfoLevel("-g", "--debug-info=line-only", "main-file.spice") == DebugInfoLevel::LINE_ONLY;
1058 1 assert parseDebugInfoLevel("--debug-info=line-only", "-g", "main-file.spice") == DebugInfoLevel::FULL;
1059 1 assert parseDebugInfoLevel("--debug-info=full", "--debug-info=none", "main-file.spice") == DebugInfoLevel::NONE;
1060
1061 // Code coverage implies debug line info, but keeps an explicitly chosen level
1062 1 Driver coverage;
1063 1 string[4] coverageArgs = ["spice", "build", "--coverage", "main-file.spice"];
1064 1 assert parseAndEnrich(coverage, 4, coverageArgs);
1065 1 assert coverage.cliOptions.instrumentation.codeCoverage;
1066 1 assert coverage.cliOptions.instrumentation.debugInfoLevel == DebugInfoLevel::LINE_ONLY;
1067 1 assert coverage.cliOptions.instrumentation.emitsDebugInfo();
1068 1 assert !coverage.cliOptions.instrumentation.emitsFullDebugInfo();
1069 1 Driver coverageFull;
1070 1 string[5] coverageFullArgs = ["spice", "build", "--coverage", "--debug-info=full", "main-file.spice"];
1071 1 assert parseAndEnrich(coverageFull, 5, coverageFullArgs);
1072 1 assert coverageFull.cliOptions.instrumentation.debugInfoLevel == DebugInfoLevel::FULL;
1073 1 assert coverageFull.cliOptions.instrumentation.emitsFullDebugInfo();
1074
1075 // Sanitizers imply other options
1076 1 Driver typeSanitizer;
1077 1 string[5] typeSanitizerArgs = ["spice", "build", "--sanitizer", "type", "main-file.spice"];
1078 1 assert parseAndEnrich(typeSanitizer, 5, typeSanitizerArgs);
1079 1 assert typeSanitizer.cliOptions.instrumentation.sanitizer == Sanitizer::TYPE;
1080 1 assert typeSanitizer.cliOptions.useTBAAMetadata;
1081
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 if isLinux() { // The memory sanitizer is only supported for Linux targets
1082 1 Driver memorySanitizer;
1083 1 string[5] memorySanitizerArgs = ["spice", "build", "--sanitizer", "memory", "main-file.spice"];
1084 1 assert parseAndEnrich(memorySanitizer, 5, memorySanitizerArgs);
1085 1 assert memorySanitizer.cliOptions.instrumentation.sanitizer == Sanitizer::MEMORY;
1086 1 assert memorySanitizer.cliOptions.useLifetimeMarkers;
1087 1 }
1088 1 return true;
1089 1 }
1090
1091 1 #[test]
1092 f<bool> testBuildVarsAndTarget() {
1093 // Build vars, which are inferred from other options
1094 1 Driver release;
1095 1 string[6] releaseArgs = ["spice", "build", "-m", "release", "--static", "main-file.spice"];
1096 1 assert parseAndEnrich(release, 6, releaseArgs);
1097 1 assert release.cliOptions.buildVars.get(String("spice.is_debug")) == "false";
1098 1 assert release.cliOptions.buildVars.get(String("spice.is_release")) == "true";
1099 1 assert release.cliOptions.buildVars.get(String("spice.is_test")) == "false";
1100 1 assert release.cliOptions.buildVars.get(String("spice.link_static")) == "true";
1101 1 assert release.cliOptions.buildVars.get(String("spice.backend")) == "llvm";
1102 1 assert release.cliOptions.buildVars.contains(String("spice.is_native_target"));
1103 1 assert release.cliOptions.buildVars.contains(String("spice.target_triple"));
1104
1105 // The TPDE backend can only be selected, if the compiler is backed by TPDE
1106
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 if tpde::isAvailable() {
1107 1 Driver tpdeBackend;
1108 1 string[7] tpdeBackendArgs = ["spice", "build", "--backend", "TPDE", "--target", "x86_64-unknown-linux-gnu", "main-file.spice"];
1109 1 assert parseAndEnrich(tpdeBackend, 7, tpdeBackendArgs);
1110 1 assert tpdeBackend.cliOptions.backend == Backend::TPDE;
1111 1 assert tpdeBackend.cliOptions.buildVars.get(String("spice.backend")) == "tpde";
1112 1 }
1113
1114 // A build var given on the command line is overwritten by the inferred one
1115 1 Driver overwritten;
1116 1 string[7] overwrittenArgsWithFile = ["spice", "build", "-b", "spice.is_debug=maybe", "-b", "a=b", "main-file.spice"];
1117 1 assert parseAndEnrich(overwritten, 7, overwrittenArgsWithFile);
1118 1 assert overwritten.cliOptions.buildVars.get(String("spice.is_debug")) == "true";
1119 1 assert overwritten.cliOptions.buildVars.get(String("a")) == "b";
1120
1121 // The target is derived from the triple
1122 1 Driver target;
1123 1 string[5] targetArgs = ["spice", "build", "--target", "aarch64-unknown-linux-gnu", "main-file.spice"];
1124 1 assert parseAndEnrich(target, 5, targetArgs);
1125 1 assert target.cliOptions.targetArch == "aarch64";
1126 1 assert target.cliOptions.targetOs == "linux";
1127 1 assert target.cliOptions.buildVars.get(String("spice.target_triple")) == "aarch64-unknown-linux-gnu";
1128
1129 // Stack traces are only printed for native targets and if the output is not meant to be comparable
1130 1 CliOptions cliOptions;
1131 1 assert cliOptions.printsStackTraceOnAbort();
1132 1 cliOptions.isNativeTarget = false; // Cross-compiling has no libbacktrace to link against
1133 1 assert !cliOptions.printsStackTraceOnAbort();
1134 1 cliOptions.isNativeTarget = true;
1135 1 cliOptions.comparableOutput = true; // Frames and offsets differ between platforms
1136 1 assert !cliOptions.printsStackTraceOnAbort();
1137 1 return true;
1138 1 }
1139
1140 1 #[test]
1141 f<bool> testOutputFileExtensions() {
1142 1 CliOptions cliOptions;
1143 1 cliOptions.targetTriple = llvm::Triple(String("x86_64"), String("unknown"), String("linux"));
1144 1 assert getOutputFileExtension(cliOptions, OutputContainer::EXECUTABLE) == "";
1145 1 assert getOutputFileExtension(cliOptions, OutputContainer::OBJECT_FILE) == "o";
1146 1 assert getOutputFileExtension(cliOptions, OutputContainer::STATIC_LIBRARY) == "a";
1147 1 assert getOutputFileExtension(cliOptions, OutputContainer::SHARED_LIBRARY) == "so";
1148 1 cliOptions.targetTriple = llvm::Triple(String("arm64"), String("apple"), String("darwin"));
1149 1 assert getOutputFileExtension(cliOptions, OutputContainer::EXECUTABLE) == "";
1150 1 assert getOutputFileExtension(cliOptions, OutputContainer::SHARED_LIBRARY) == "dylib";
1151 // All Apple OSes are Darwin based, like in llvm::Triple::isOSDarwin()
1152 1 cliOptions.targetTriple = llvm::Triple(String("aarch64"), String("apple"), String("ios17.0"));
1153 1 assert getOutputFileExtension(cliOptions, OutputContainer::SHARED_LIBRARY) == "dylib";
1154 1 cliOptions.targetTriple = llvm::Triple(String("aarch64"), String("apple"), String("tvos"));
1155 1 assert getOutputFileExtension(cliOptions, OutputContainer::SHARED_LIBRARY) == "dylib";
1156 1 cliOptions.targetTriple = llvm::Triple(String("arm64"), String("apple"), String("watchos"));
1157 1 assert getOutputFileExtension(cliOptions, OutputContainer::SHARED_LIBRARY) == "dylib";
1158 1 cliOptions.targetTriple = llvm::Triple(String("aarch64"), String("apple"), String("visionos"));
1159 1 assert getOutputFileExtension(cliOptions, OutputContainer::SHARED_LIBRARY) == "dylib";
1160 1 cliOptions.targetTriple = llvm::Triple(String("aarch64"), String("apple"), String("driverkit"));
1161 1 assert getOutputFileExtension(cliOptions, OutputContainer::SHARED_LIBRARY) == "dylib";
1162 1 cliOptions.targetTriple = llvm::Triple(String("x86_64"), String("pc"), String("windows"));
1163 1 assert getOutputFileExtension(cliOptions, OutputContainer::EXECUTABLE) == "exe";
1164 1 assert getOutputFileExtension(cliOptions, OutputContainer::OBJECT_FILE) == "obj";
1165 1 assert getOutputFileExtension(cliOptions, OutputContainer::STATIC_LIBRARY) == "lib";
1166 1 assert getOutputFileExtension(cliOptions, OutputContainer::SHARED_LIBRARY) == "dll";
1167 1 cliOptions.targetTriple = llvm::Triple(String("wasm32"), String("unknown"), String("unknown"));
1168 1 assert getOutputFileExtension(cliOptions, OutputContainer::EXECUTABLE) == "wasm";
1169 1 return true;
1170 1 }
1171
1172 1 #[test]
1173 f<bool> testRejectedArguments() {
1174 // The source file has to exist for uninstall. For the compile path, the missing file is reported by the pipeline
1175 1 Driver missingFile;
1176 1 string[3] missingFileArgs = ["spice", "uninstall", "does-not-exist.spice"];
1177 1 assert parseOnly(missingFile, 3, missingFileArgs) != EXIT_CODE_SUCCESS;
1178
1179 // The source file is required
1180 1 Driver noFile;
1181 1 string[2] noFileArgs = ["spice", "build"];
1182 1 assert parseOnly(noFile, 2, noFileArgs) != EXIT_CODE_SUCCESS;
1183
1184 // Unknown arguments are rejected. Nothing is compiled then
1185 1 Driver unknownOption;
1186 1 string[4] unknownOptionArgs = ["spice", "build", "--unknown-option", "x.spice"];
1187 1 assert parseOnly(unknownOption, 4, unknownOptionArgs) != EXIT_CODE_SUCCESS;
1188 1 assert !unknownOption.shouldCompile;
1189 1 return true;
1190 1 }
1191
1192 1 #[test]
1193 f<bool> testEqualsSyntaxOptionValues() {
1194 // Option values can also be attached with '='
1195 1 Driver equalsSyntax;
1196 1 string[7] equalsSyntaxArgs = ["spice", "build", "--sanitizer=address", "--output-container=exec", "--backend=llvm", "-q", "main-file.spice"];
1197 1 assert parseAndEnrich(equalsSyntax, 7, equalsSyntaxArgs);
1198 1 assert equalsSyntax.cliOptions.instrumentation.sanitizer == Sanitizer::ADDRESS;
1199 1 assert equalsSyntax.cliOptions.outputContainer == OutputContainer::EXECUTABLE;
1200 1 assert equalsSyntax.cliOptions.backend == Backend::LLVM;
1201 1 assert equalsSyntax.cliOptions.buildVars.get(String("spice.backend")) == "llvm";
1202 1 assert equalsSyntax.cliOptions.quiet;
1203 1 assert equalsSyntax.cliOptions.useLifetimeMarkers; // implicitly due to the enabled address sanitizer
1204
1205 // Enum values are case-insensitive
1206 1 Driver upperCase;
1207 1 string[6] upperCaseArgs = ["spice", "build", "--build-mode=RELEASE", "--sanitizer=Thread", "--output-container=OBJ", "main-file.spice"];
1208 1 assert parseAndEnrich(upperCase, 6, upperCaseArgs);
1209 1 assert upperCase.cliOptions.buildMode == BuildMode::RELEASE;
1210 1 assert upperCase.cliOptions.instrumentation.sanitizer == Sanitizer::THREAD;
1211 1 assert upperCase.cliOptions.outputContainer == OutputContainer::OBJECT_FILE;
1212
1213 // The debug info level without a value does not swallow the source file
1214 1 Driver debugInfoWithoutValue;
1215 1 string[4] debugInfoWithoutValueArgs = ["spice", "build", "--debug-info", "main-file.spice"];
1216 1 assert parseAndEnrich(debugInfoWithoutValue, 4, debugInfoWithoutValueArgs);
1217 1 assert debugInfoWithoutValue.cliOptions.instrumentation.debugInfoLevel == DebugInfoLevel::FULL;
1218 1 assert debugInfoWithoutValue.cliOptions.mainSourceFile.toGenericString() == "main-file.spice";
1219 1 return true;
1220 1 }
1221