source-file.spice
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Copyright (c) 2021-2026 ChilliBits. All rights reserved. | ||
| 2 | |||
| 3 | // Std imports | ||
| 4 | import "std/text/print"; | ||
| 5 | import "std/text/stringstream"; | ||
| 6 | import "std/text/format"; | ||
| 7 | import "std/type/any"; | ||
| 8 | import "std/type/type-conversion"; | ||
| 9 | import "std/data/pair"; | ||
| 10 | import "std/data/map"; | ||
| 11 | import "std/data/unordered-map"; | ||
| 12 | import "std/data/unordered-set"; | ||
| 13 | import "std/data/vector"; | ||
| 14 | import "std/io/file"; | ||
| 15 | import "std/io/filepath"; | ||
| 16 | import "std/text/json/json-value"; | ||
| 17 | import "std/text/json/json-serializer"; | ||
| 18 | import "std/time/timer"; | ||
| 19 | import "std/os/thread-pool"; | ||
| 20 | import "std/bindings/llvm/llvm" as llvm; | ||
| 21 | |||
| 22 | // Own imports | ||
| 23 | import "@/exception/fatal-error"; | ||
| 24 | import "@/driver"; | ||
| 25 | import "@/global/global-resource-manager"; | ||
| 26 | import "@/global/runtime-module-manager"; | ||
| 27 | import "@/ast/ast-nodes"; | ||
| 28 | import "@/exception/compiler-error"; | ||
| 29 | import "@/exception/error-manager"; | ||
| 30 | import "@/exception/semantic-error"; | ||
| 31 | import "@/model/function" as model; | ||
| 32 | import "@/util/compiler-warning"; | ||
| 33 | import "@/util/system-util"; | ||
| 34 | import "@/util/compile-progress"; | ||
| 35 | import "@/symboltablebuilder/symbol-table-entry"; | ||
| 36 | import "@/symboltablebuilder/scope"; | ||
| 37 | import "@/symboltablebuilder/qual-type"; | ||
| 38 | import "@/symboltablebuilder/super-type"; | ||
| 39 | import "@/model/struct"; | ||
| 40 | import "@/symboltablebuilder/type" as symboltable; // Aliased, because Type clashes with llvm::Type | ||
| 41 | import "@/lexer/lexer"; | ||
| 42 | import "@/parser/parser"; | ||
| 43 | import "@/importcollector/import-collector"; | ||
| 44 | import "@/symboltablebuilder/symbol-table-builder"; | ||
| 45 | import "@/typechecker/type-checker"; | ||
| 46 | import "@/typechecker/post-type-checking-verifier"; | ||
| 47 | import "@/linter/lint-finding"; | ||
| 48 | import "@/linter/lint-pass"; | ||
| 49 | import "@/irgenerator/ir-generator"; | ||
| 50 | import "@/iroptimizer/ir-optimizer"; | ||
| 51 | import "@/linker/bitcode-linker"; | ||
| 52 | import "@/objectemitter/abstract-object-emitter"; | ||
| 53 | import "@/util/concurrency"; | ||
| 54 | import "@/objectemitter/llvm-object-emitter"; | ||
| 55 | import "@/objectemitter/tpde-object-emitter"; | ||
| 56 | import "@/typechecker/function-manager"; | ||
| 57 | import "@/typechecker/struct-manager"; | ||
| 58 | import "@/typechecker/interface-manager"; | ||
| 59 | import "@/typechecker/union-manager"; | ||
| 60 | import "@/visualizer/ast-visualizer"; | ||
| 61 | import "@/visualizer/dependency-graph-visualizer"; | ||
| 62 | import "@/util/test-util"; | ||
| 63 | |||
| 64 | type CompileStageType enum { | ||
| 65 | NONE, | ||
| 66 | LEXER_AND_PARSER, | ||
| 67 | AST_VISUALIZER, | ||
| 68 | IMPORT_COLLECTOR, | ||
| 69 | SYMBOL_TABLE_BUILDER, | ||
| 70 | TYPE_CHECKER_PRE, | ||
| 71 | TYPE_CHECKER_POST, | ||
| 72 | DEP_GRAPH_VISUALIZER, | ||
| 73 | IR_GENERATOR, | ||
| 74 | IR_OPTIMIZER, | ||
| 75 | OBJECT_EMITTER, | ||
| 76 | FINISHED | ||
| 77 | } | ||
| 78 | |||
| 79 | type CompileStageIOType enum { | ||
| 80 | IO_CODE, | ||
| 81 | IO_AST, | ||
| 82 | IO_IR, | ||
| 83 | IO_OBJECT_FILE | ||
| 84 | } | ||
| 85 | |||
| 86 | 3971 | type TimerOutput struct { | |
| 87 | 3971 | unsigned long lexerAndParser = 0l | |
| 88 | 3971 | unsigned long astVisualizer = 0l | |
| 89 | 3971 | unsigned long importCollector = 0l | |
| 90 | 3971 | unsigned long symbolTableBuilder = 0l | |
| 91 | 3971 | unsigned long typeCheckerPre = 0l | |
| 92 | 3971 | unsigned long typeCheckerPost = 0l | |
| 93 | 3971 | unsigned long depGraphVisualizer = 0l | |
| 94 | 3971 | unsigned long irGenerator = 0l | |
| 95 | 3971 | unsigned long irOptimizer = 0l | |
| 96 | 3971 | unsigned long objectEmitter = 0l | |
| 97 | } | ||
| 98 | |||
| 99 | /** | ||
| 100 | * Collects the output of the compiler for debugging | ||
| 101 | */ | ||
| 102 | 7493 | public type CompilerOutput struct { | |
| 103 | String astString | ||
| 104 | String symbolTableString | ||
| 105 | String depGraphString | ||
| 106 | String irString | ||
| 107 | String irOptString | ||
| 108 | String asmString | ||
| 109 | String typesString | ||
| 110 | String cacheStats | ||
| 111 | public Vector<CompilerWarning> warnings | ||
| 112 | public Vector<LintFinding> lintFindings | ||
| 113 | TimerOutput times | ||
| 114 | } | ||
| 115 | |||
| 116 | public type NameRegistryEntry struct { | ||
| 117 | public unsigned long typeId // Set for structs, interfaces and enums | ||
| 118 | public SymbolTableEntry* targetEntry | ||
| 119 | public Scope* targetScope | ||
| 120 | public SymbolTableEntry* importEntry = nil<SymbolTableEntry*> | ||
| 121 | } | ||
| 122 | |||
| 123 | /** | ||
| 124 | * Get the code generation opt level for the given opt level | ||
| 125 | * | ||
| 126 | * @param optLevel Opt level | ||
| 127 | * @return Code generation opt level | ||
| 128 | */ | ||
| 129 | 3971 | f<llvm::LLVMCodeGenOptLevel> getCodeGenOptLevel(OptLevel optLevel) { | |
| 130 | 3971 | switch optLevel { | |
| 131 | 3874 | case OptLevel::O0: { return llvm::LLVMCodeGenOptLevel::None; } | |
| 132 | 1 | case OptLevel::O1: { return llvm::LLVMCodeGenOptLevel::Less; } | |
| 133 | 9 | case OptLevel::O3: { return llvm::LLVMCodeGenOptLevel::Aggressive; } | |
| 134 | 87 | default: { return llvm::LLVMCodeGenOptLevel::Default; } // O2, Os, Oz | |
| 135 | } | ||
| 136 | ✗ | } | |
| 137 | |||
| 138 | /** | ||
| 139 | * Represents a single source file | ||
| 140 | */ | ||
| 141 | 3522 | public type SourceFile struct { | |
| 142 | // Public fields | ||
| 143 | public String name | ||
| 144 | public String fileName | ||
| 145 | public FilePath filePath | ||
| 146 | public String fileDir | ||
| 147 | 3971 | public bool isStdFile = false | |
| 148 | 3971 | public bool isMainFile = true | |
| 149 | 3971 | public bool alwaysKeepSymbolsOnNameCollision = false | |
| 150 | 3971 | public bool ignoreWarnings = false | |
| 151 | 3971 | public bool explicitErrorHandling = false | |
| 152 | 3971 | public bool errorReturnTracing = false | |
| 153 | 3971 | public bool reVisitRequested = true | |
| 154 | 3971 | public CompileStageType previousStage = CompileStageType::NONE | |
| 155 | public CompilerOutput compilerOutput | ||
| 156 | public SourceFile* parent | ||
| 157 | 3971 | public ASTEntryNode* ast = nil<ASTEntryNode*> | |
| 158 | 3971 | public Scope* globalScope = nil<Scope*> // Owned by the scope pool of the resource manager | |
| 159 | public llvm::LLVMContext context | ||
| 160 | public llvm::IRBuilder builder | ||
| 161 | public llvm::TargetMachine targetMachine | ||
| 162 | public llvm::Module llvmModule | ||
| 163 | public FilePath objectFilePath | ||
| 164 | public String objectFileStem // Name of the object file without extension, unique within the compilation | ||
| 165 | public Map<String, SourceFile*> dependencies // Has to be an ordered map to keep the compilation order deterministic | ||
| 166 | public Vector<const SourceFile*> dependants | ||
| 167 | public Map<String, NameRegistryEntry> exportedNameRegistry | ||
| 168 | // Unqualified names that were dropped from the name registry, because multiple imports expose them. Maps each name to the | ||
| 169 | // import entries exposing it, so that a failing lookup can point the user to the colliding imports | ||
| 170 | public Map<String, Vector<const SymbolTableEntry*>> ambiguousNameRegistry | ||
| 171 | public Vector<const model::Function*> testFunctions | ||
| 172 | public GlobalResourceManager& resourceManager // Public, because Scope and SymbolTable need access | ||
| 173 | // Private fields | ||
| 174 | CliOptions& cliOptions | ||
| 175 | UnorderedMap<long, llvm::Type> typeToLLVMTypeMapping // Maps the address of each type to its LLVM type | ||
| 176 | // Number of name registry entries per target entry address, to answer isStructKnownByNameRegistry without scanning the registry | ||
| 177 | UnorderedMap<long, unsigned long> nameRegistryTargetEntryCounts | ||
| 178 | 3971 | public unsigned int importedRuntimeModules = 0 // Public, because the RuntimeModuleManager needs access | |
| 179 | 3971 | unsigned short totalTypeCheckerRuns = 0s | |
| 180 | // Cycle-safety guards: the pipeline drivers recurse over a dependency graph that may contain cycles (circular | ||
| 181 | // imports). These flags prevent re-entering a file that is already being processed in the same stage. | ||
| 182 | 3971 | bool registriesMerged = false | |
| 183 | 3971 | bool typeCheckerPreRunning = false | |
| 184 | 3971 | bool typeCheckerPostRunning = false | |
| 185 | 3971 | bool backEndStarted = false | |
| 186 | 3971 | bool warningsCollected = false | |
| 187 | 3971 | bool lintFindingsCollected = false | |
| 188 | } | ||
| 189 | |||
| 190 | 3971 | public p SourceFile.ctor(GlobalResourceManager &resourceManager, SourceFile* parent, const String& name, const FilePath& filePath, bool isStdFile) { | |
| 191 | // Copy data | ||
| 192 | 3971 | this.name = name; | |
| 193 | 3971 | this.filePath = filePath; | |
| 194 | 3971 | this.isStdFile = isStdFile; | |
| 195 | 3971 | this.parent = parent; | |
| 196 | 3971 | this.builder = llvm::IRBuilder(resourceManager.cliOptions.useLTO ? resourceManager.ltoContext : this.context); | |
| 197 | 3971 | this.resourceManager = resourceManager; | |
| 198 | 3971 | this.cliOptions = resourceManager.cliOptions; | |
| 199 | |||
| 200 | // Deduce fileName and fileDir | ||
| 201 | 3971 | const FilePath path = FilePath(filePath); | |
| 202 | 3971 | this.fileName = path.getFileName(); | |
| 203 | 3971 | this.fileDir = path.getParentDir(); | |
| 204 | 3971 | this.objectFileStem = resourceManager.reserveObjectFileStem(path); | |
| 205 | |||
| 206 | // Discard value names if not required | ||
| 207 | 3971 | this.context.setDiscardValueNames(!this.cliOptions.namesForIRValues); | |
| 208 | |||
| 209 | // Search after selected target | ||
| 210 | 3971 | string error = nil<string>; | |
| 211 | 3971 | const llvm::Target target = llvm::getTargetFromTriple(this.cliOptions.targetTriple, &error); | |
| 212 | 3971 | if error != nil<string> { | |
| 213 | ✗ | const String messageSuffix = "Selected target was not found: " + String(error); | |
| 214 | ✗ | const CompilerError compilerError = CompilerError(CompilerErrorType::TARGET_NOT_AVAILABLE, messageSuffix.getRaw()); | |
| 215 | ✗ | abortWithError(compilerError.getMessage()); | |
| 216 | ✗ | } | |
| 217 | |||
| 218 | // Create target machine | ||
| 219 | 3971 | const string cpuName = resourceManager.cpuName; | |
| 220 | 3971 | const string features = resourceManager.cpuFeatures; | |
| 221 | 3971 | const llvm::Triple& targetTriple = this.cliOptions.targetTriple; | |
| 222 | 3971 | const llvm::LLVMRelocMode relocMode = llvm::LLVMRelocMode::PIC; | |
| 223 | // The LLVM C API cannot change the codegen opt level of a target machine, so it is set right away here. | ||
| 224 | 3971 | const llvm::LLVMCodeGenOptLevel codeGenOptLevel = getCodeGenOptLevel(this.cliOptions.optLevel); | |
| 225 | 3971 | this.targetMachine = target.createTargetMachine(targetTriple, cpuName, features, codeGenOptLevel, relocMode, llvm::LLVMCodeModel::Default); | |
| 226 | // Emit the assembly code with comments. This only affects the assembly code, not the object files | ||
| 227 | 3971 | this.targetMachine.setAsmVerbosity(true); | |
| 228 | 3971 | } | |
| 229 | |||
| 230 | 5037 | public p SourceFile.runParser() { | |
| 231 | // Check if this stage has already been done | ||
| 232 | 5037 | if this.previousStage >= CompileStageType::LEXER_AND_PARSER { | |
| 233 | 1086 | return; | |
| 234 | } | ||
| 235 | |||
| 236 | |||
| 237 | 3951 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.lexerAndParser); | |
| 238 | 3951 | timer.start(); | |
| 239 | |||
| 240 | // The source file must exist. Raise the same error as the lexer stage does when it cannot be read. | ||
| 241 | 3951 | if !this.filePath.exists() { | |
| 242 | 1 | StringStream msg; | |
| 243 | 1 | msg << "Source file at path '" << this.filePath.toString() << "' does not exist."; | |
| 244 | 1 | const String messageSuffix = msg.str(); | |
| 245 | 1 | const CompilerError error = CompilerError(CompilerErrorType::SOURCE_FILE_NOT_FOUND, messageSuffix.getRaw()); | |
| 246 | 1 | abortWithError(error.getMessage()); | |
| 247 | 1 | } | |
| 248 | |||
| 249 | // Parse the source file | ||
| 250 | 3951 | Lexer lexer = Lexer(this.filePath); | |
| 251 | 3951 | Parser parser = Parser(this.resourceManager, this, lexer); | |
| 252 | 3951 | this.ast = parser.parse(); | |
| 253 | 3951 | assert this.ast != nil<ASTEntryNode*>; | |
| 254 | |||
| 255 | // Create the global scope | ||
| 256 | 3951 | this.globalScope = this.resourceManager.createScope(nil<Scope*>, this, ScopeType::GLOBAL, &this.ast.node.codeLoc); | |
| 257 | |||
| 258 | 3951 | this.previousStage = CompileStageType::LEXER_AND_PARSER; | |
| 259 | 3951 | timer.stop(); | |
| 260 | 3951 | this.printStatusMessage("Parser", CompileStageIOType::IO_CODE, CompileStageIOType::IO_AST, this.compilerOutput.times.lexerAndParser); | |
| 261 | 3951 | } | |
| 262 | |||
| 263 | 5037 | public p SourceFile.runASTVisualizer() { | |
| 264 | // Only execute if enabled | ||
| 265 | 5037 | if !this.cliOptions.dump.dumpAST && !this.cliOptions.testMode { | |
| 266 | 26 | return; | |
| 267 | } | ||
| 268 | // Check if this stage has already been done | ||
| 269 | 5011 | if this.previousStage >= CompileStageType::AST_VISUALIZER { | |
| 270 | 1079 | return; | |
| 271 | } | ||
| 272 | |||
| 273 | 3932 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.astVisualizer); | |
| 274 | 3932 | timer.start(); | |
| 275 | |||
| 276 | // Generate dot code for this source file | ||
| 277 | 3932 | StringStream dotCode; | |
| 278 | 3932 | this.visualizerPreamble(dotCode); | |
| 279 | 3932 | ASTVisualizer astVisualizer = ASTVisualizer(); | |
| 280 | 3932 | Any astDotCode = astVisualizer.visit(this.ast); | |
| 281 | 3932 | dotCode << " " << astDotCode.get<String>() << "}"; | |
| 282 | |||
| 283 | // Dump the serialized AST string and the SVG file | ||
| 284 | 3932 | this.compilerOutput.astString = dotCode.str(); | |
| 285 | |||
| 286 | 3932 | if this.cliOptions.dump.dumpAST { | |
| 287 | 7 | this.visualizerOutput("AST", this.compilerOutput.astString); | |
| 288 | 7 | } | |
| 289 | |||
| 290 | 3932 | this.previousStage = CompileStageType::AST_VISUALIZER; | |
| 291 | 3932 | timer.stop(); | |
| 292 | 3932 | this.printStatusMessage("AST Visualizer", CompileStageIOType::IO_AST, CompileStageIOType::IO_AST, this.compilerOutput.times.astVisualizer); | |
| 293 | 3932 | } | |
| 294 | |||
| 295 | 5029 | public p SourceFile.runImportCollector() { | |
| 296 | // Skip if this stage has already been done | ||
| 297 | 5029 | if this.previousStage >= CompileStageType::IMPORT_COLLECTOR { | |
| 298 | 1086 | return; | |
| 299 | } | ||
| 300 | |||
| 301 | 3943 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.importCollector); | |
| 302 | 3943 | timer.start(); | |
| 303 | |||
| 304 | // Collect the imports for this source file | ||
| 305 | 3943 | ImportCollector importCollector = ImportCollector(this.resourceManager, this); | |
| 306 | 3943 | importCollector.visit(this.ast); | |
| 307 | 3943 | this.checkForHardError(); | |
| 308 | |||
| 309 | 3943 | this.previousStage = CompileStageType::IMPORT_COLLECTOR; | |
| 310 | |||
| 311 | // Run first part of pipeline for the imported source file | ||
| 312 | 6451 | foreach dyn dependency : this.dependencies { | |
| 313 | 2508 | SourceFile* sourceFile = dependency.getSecond(); | |
| 314 | 2508 | sourceFile.runFrontEnd(); | |
| 315 | 2508 | } | |
| 316 | |||
| 317 | 3943 | timer.stop(); | |
| 318 | 3943 | this.printStatusMessage("Import Collector", CompileStageIOType::IO_AST, CompileStageIOType::IO_AST, this.compilerOutput.times.importCollector); | |
| 319 | 3943 | } | |
| 320 | |||
| 321 | 5029 | public p SourceFile.runSymbolTableBuilder() { | |
| 322 | // Skip if this stage has already been done | ||
| 323 | 5029 | if this.previousStage >= CompileStageType::SYMBOL_TABLE_BUILDER { | |
| 324 | 1086 | return; | |
| 325 | } | ||
| 326 | |||
| 327 | 3943 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.symbolTableBuilder); | |
| 328 | 3943 | timer.start(); | |
| 329 | |||
| 330 | // Build the symbol table. The dependencies' exported name registries are merged in afterwards, in a separate pass | ||
| 331 | // (mergeNameRegistriesRecursive), once every reachable file has built its own registry. This deferral is what makes | ||
| 332 | // circular imports work: with a cycle, a dependency's registry is not fully populated yet at this point. | ||
| 333 | 3943 | SymbolTableBuilder symbolTableBuilder = SymbolTableBuilder(this.resourceManager, this); | |
| 334 | 3943 | symbolTableBuilder.visit(this.ast); | |
| 335 | 3943 | this.checkForHardError(); | |
| 336 | |||
| 337 | 3943 | this.previousStage = CompileStageType::SYMBOL_TABLE_BUILDER; | |
| 338 | 3943 | this.resourceManager.compileProgress.finishFrontEnd(); | |
| 339 | 3943 | timer.stop(); | |
| 340 | 3943 | this.printStatusMessage("Symbol Table Builder", CompileStageIOType::IO_AST, CompileStageIOType::IO_AST, this.compilerOutput.times.symbolTableBuilder); | |
| 341 | 3943 | } | |
| 342 | |||
| 343 | 5000 | public p SourceFile.runTypeCheckerPre() { // Public for the RuntimeModuleManager | |
| 344 | // The typeCheckerPreRunning guard breaks the recursion on a circular import: a cyclic back-edge returns immediately | ||
| 345 | // instead of recursing forever. | ||
| 346 | 5000 | if this.previousStage >= CompileStageType::TYPE_CHECKER_PRE || this.typeCheckerPreRunning { | |
| 347 | 1084 | return; | |
| 348 | } | ||
| 349 | 3916 | this.typeCheckerPreRunning = true; | |
| 350 | |||
| 351 | // Type-check all dependencies first | ||
| 352 | 6417 | foreach dyn dependency : this.dependencies { | |
| 353 | 2501 | SourceFile* sourceFile = dependency.getSecond(); | |
| 354 | 2501 | sourceFile.runTypeCheckerPre(); | |
| 355 | 2501 | } | |
| 356 | |||
| 357 | 3916 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.typeCheckerPre); | |
| 358 | 3916 | timer.start(); | |
| 359 | |||
| 360 | // Then type-check the current file | ||
| 361 | 3916 | TypeChecker typeChecker = TypeChecker(this.resourceManager, this, TypeCheckerMode::TC_MODE_PRE); | |
| 362 | 3916 | typeChecker.visit(this.ast); | |
| 363 | 3916 | this.checkForHardError(); | |
| 364 | |||
| 365 | 3916 | this.previousStage = CompileStageType::TYPE_CHECKER_PRE; | |
| 366 | 3916 | this.resourceManager.compileProgress.finishTypeCheckerPre(); | |
| 367 | 3916 | this.typeCheckerPreRunning = false; | |
| 368 | 3916 | timer.stop(); | |
| 369 | 3916 | this.printStatusMessage("Type Checker Pre", CompileStageIOType::IO_AST, CompileStageIOType::IO_AST, this.compilerOutput.times.typeCheckerPre); | |
| 370 | 3916 | } | |
| 371 | |||
| 372 | 12150 | p SourceFile.runTypeCheckerPost() { | |
| 373 | // Re-entrancy guard: within an import cycle, a dependency's post-run recurses back into this file's post-run. The | ||
| 374 | // in-flight fixpoint loop below already revisits this file, so the nested call must be a no-op to avoid unbounded | ||
| 375 | // mutual recursion. Convergence is driven by the reVisitRequested flags propagating across the cycle. | ||
| 376 | 12150 | if this.typeCheckerPostRunning { | |
| 377 | 395 | return; | |
| 378 | } | ||
| 379 | |||
| 380 | // Nothing to do if this file already finished the post stage and nobody asked for another visit since. This is the | ||
| 381 | // common case, because every visit of a file runs the post stage of all its dependencies again. | ||
| 382 | 11755 | if !this.reVisitRequested && this.previousStage >= CompileStageType::TYPE_CHECKER_POST { | |
| 383 | 5813 | return; | |
| 384 | } | ||
| 385 | |||
| 386 | // Skip if not all dependants finished type checking yet | ||
| 387 | 5942 | if !this.haveAllDependantsBeenTypeChecked() { | |
| 388 | 2246 | return; | |
| 389 | } | ||
| 390 | |||
| 391 | 3696 | this.typeCheckerPostRunning = true; | |
| 392 | |||
| 393 | 3696 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.typeCheckerPost); | |
| 394 | 3696 | timer.start(); | |
| 395 | |||
| 396 | // Start type-checking loop. The type-checker can request a re-execution. The max number of type-checker runs is limited | ||
| 397 | 3696 | TypeChecker typeChecker = TypeChecker(this.resourceManager, this, TypeCheckerMode::TC_MODE_POST); | |
| 398 | 3696 | unsigned short typeCheckerRuns = 0s; | |
| 399 | 10080 | while this.reVisitRequested { | |
| 400 | 6384 | typeCheckerRuns++; | |
| 401 | 6384 | this.totalTypeCheckerRuns++; | |
| 402 | 6384 | this.reVisitRequested = false; | |
| 403 | |||
| 404 | // Type-check the current file first. Multiple times, if requested | ||
| 405 | 6384 | timer.resume(); | |
| 406 | 6384 | typeChecker.visit(this.ast); | |
| 407 | 6384 | this.checkForHardError(); | |
| 408 | 6384 | timer.pause(); | |
| 409 | |||
| 410 | // Then type-check all dependencies | ||
| 411 | 17766 | foreach dyn dependency : this.dependencies { | |
| 412 | 11382 | SourceFile* sourceFile = dependency.getSecond(); | |
| 413 | 11382 | sourceFile.runTypeCheckerPost(); | |
| 414 | 11382 | } | |
| 415 | 6384 | } | |
| 416 | |||
| 417 | 3696 | this.typeCheckerPostRunning = false; | |
| 418 | |||
| 419 | // Measure the remaining work of this stage as well. The time spent in the dependencies above is not part of it | ||
| 420 | 3696 | timer.resume(); | |
| 421 | 3696 | this.checkForSoftErrors(); | |
| 422 | |||
| 423 | // Check if all dyn variables were type-inferred successfully | ||
| 424 | 3696 | this.globalScope.ensureSuccessfulTypeInference(); | |
| 425 | |||
| 426 | // In debug builds, verify that the TypeChecker fully annotated the AST | ||
| 427 | if __get_build_var<bool>("spice.is_debug") { | ||
| 428 | 3696 | this.runPostTypeCheckingVerifier(); | |
| 429 | } | ||
| 430 | |||
| 431 | // A revisit request can run the post stage again later on, but it only counts as progress the first time | ||
| 432 | 3696 | if this.previousStage < CompileStageType::TYPE_CHECKER_POST { | |
| 433 | 3535 | this.resourceManager.compileProgress.finishTypeCheckerPost(); | |
| 434 | 3535 | } | |
| 435 | 3696 | this.previousStage = CompileStageType::TYPE_CHECKER_POST; | |
| 436 | 3696 | timer.stop(); | |
| 437 | 3696 | this.printStatusMessage("Type Checker Post", CompileStageIOType::IO_AST, CompileStageIOType::IO_AST, this.compilerOutput.times.typeCheckerPost, typeCheckerRuns); | |
| 438 | |||
| 439 | // Save the JSON version in the compiler output | ||
| 440 | 3696 | if this.cliOptions.dump.dumpSymbolTable || this.cliOptions.testMode { | |
| 441 | 3677 | JsonValue* symbolTableJSON = this.globalScope.getSymbolTableJSON(); | |
| 442 | 3677 | JsonSerializer serializer = JsonSerializer(/*pretty=*/true); | |
| 443 | 3677 | this.compilerOutput.symbolTableString = serializer.serialize(symbolTableJSON); | |
| 444 | 3677 | deleteJsonValue(symbolTableJSON); | |
| 445 | 3677 | } | |
| 446 | |||
| 447 | // Dump symbol table | ||
| 448 | 3696 | if this.cliOptions.dump.dumpSymbolTable { | |
| 449 | 16 | this.dumpOutput(this.compilerOutput.symbolTableString, String("Symbol Table"), String("symbol-table.json")); | |
| 450 | 16 | } | |
| 451 | 3696 | } | |
| 452 | |||
| 453 | 3696 | p SourceFile.runPostTypeCheckingVerifier() { | |
| 454 | 3696 | PostTypeCheckingVerifier verifier = PostTypeCheckingVerifier(this.resourceManager, this); | |
| 455 | 3696 | verifier.verify(this.ast); | |
| 456 | 3696 | } | |
| 457 | |||
| 458 | 763 | public p SourceFile.runDependencyGraphVisualizer() { | |
| 459 | // Only execute if enabled | ||
| 460 | 763 | if !this.cliOptions.dump.dumpDependencyGraph && !this.cliOptions.testMode { return; } | |
| 461 | 760 | if this.previousStage >= CompileStageType::DEP_GRAPH_VISUALIZER { return; } | |
| 462 | |||
| 463 | 760 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.depGraphVisualizer); | |
| 464 | 760 | timer.start(); | |
| 465 | |||
| 466 | // Generate dot code for the full dependency graph | ||
| 467 | 760 | StringStream dotCode; | |
| 468 | 760 | this.visualizerPreamble(dotCode); | |
| 469 | 760 | DependencyGraphVisualizer depGraphVisualizer = DependencyGraphVisualizer(this.resourceManager, this); | |
| 470 | 760 | depGraphVisualizer.getDependencyGraph(dotCode); | |
| 471 | 760 | dotCode << "}"; | |
| 472 | |||
| 473 | 760 | this.compilerOutput.depGraphString = dotCode.str(); | |
| 474 | |||
| 475 | 760 | if this.cliOptions.dump.dumpDependencyGraph { | |
| 476 | 2 | this.visualizerOutput("Dependency Graph", this.compilerOutput.depGraphString); | |
| 477 | 2 | } | |
| 478 | |||
| 479 | 760 | this.previousStage = CompileStageType::DEP_GRAPH_VISUALIZER; | |
| 480 | 760 | timer.stop(); | |
| 481 | 760 | this.printStatusMessage("Dependency Graph Visualizer", CompileStageIOType::IO_AST, CompileStageIOType::IO_AST, this.compilerOutput.times.depGraphVisualizer); | |
| 482 | 760 | } | |
| 483 | |||
| 484 | 3499 | public p SourceFile.runIRGenerator() { | |
| 485 | // Skip if this stage has already been done | ||
| 486 | 3499 | if this.previousStage >= CompileStageType::IR_GENERATOR { | |
| 487 | ✗ | return; | |
| 488 | } | ||
| 489 | |||
| 490 | 3499 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.irGenerator); | |
| 491 | 3499 | timer.start(); | |
| 492 | 3499 | this.resourceManager.compileProgress.startBackEndStage(BackEndStage::IR_GENERATOR); | |
| 493 | |||
| 494 | // Create the LLVM module for this source file | ||
| 495 | 3499 | llvm::LLVMContext& llvmContext = this.cliOptions.useLTO ? this.resourceManager.ltoContext : this.context; | |
| 496 | 3499 | this.llvmModule = llvm::Module(this.fileName.getRaw(), llvmContext); | |
| 497 | |||
| 498 | // Generate this source file | ||
| 499 | 3499 | IRGenerator irGenerator = IRGenerator(this.resourceManager, this); | |
| 500 | 3499 | irGenerator.visit(this.ast); | |
| 501 | |||
| 502 | // Save the ir string in the compiler output | ||
| 503 | 3499 | if this.cliOptions.dump.dumpIR || this.cliOptions.testMode { | |
| 504 | 3499 | this.compilerOutput.irString = getIRString(&this.llvmModule, this.cliOptions); | |
| 505 | 3499 | } | |
| 506 | |||
| 507 | // Dump unoptimized IR code | ||
| 508 | 3499 | if this.cliOptions.dump.dumpIR { | |
| 509 | 573 | this.dumpOutput(this.compilerOutput.irString, String("Unoptimized IR Code"), String("ir-code.ll")); | |
| 510 | 573 | } | |
| 511 | |||
| 512 | 3499 | this.previousStage = CompileStageType::IR_GENERATOR; | |
| 513 | 3499 | this.resourceManager.compileProgress.finishBackEndStage(BackEndStage::IR_GENERATOR); | |
| 514 | 3499 | timer.stop(); | |
| 515 | 3499 | this.printStatusMessage("IR Generator", CompileStageIOType::IO_AST, CompileStageIOType::IO_IR, this.compilerOutput.times.irGenerator); | |
| 516 | 3499 | } | |
| 517 | |||
| 518 | 3495 | public p SourceFile.runDefaultIROptimizer() { | |
| 519 | 3495 | assert !this.cliOptions.useLTO; | |
| 520 | |||
| 521 | // Skip if this stage has already been done | ||
| 522 | 3495 | if this.previousStage > CompileStageType::IR_OPTIMIZER || (this.previousStage == CompileStageType::IR_OPTIMIZER && !this.cliOptions.testMode) { | |
| 523 | ✗ | return; | |
| 524 | } | ||
| 525 | |||
| 526 | 3495 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.irOptimizer); | |
| 527 | 3495 | timer.start(); | |
| 528 | 3495 | this.resourceManager.compileProgress.startBackEndStage(BackEndStage::IR_OPTIMIZER); | |
| 529 | |||
| 530 | // Optimize this source file | ||
| 531 | 3495 | IROptimizer irOptimizer = IROptimizer(this.resourceManager, this); | |
| 532 | 3495 | irOptimizer.prepare(); | |
| 533 | 3495 | irOptimizer.optimizeDefault(); | |
| 534 | |||
| 535 | // Save the optimized ir string in the compiler output | ||
| 536 | 3495 | if this.cliOptions.dump.dumpIR || this.cliOptions.testMode { | |
| 537 | 3495 | this.compilerOutput.irOptString = getIRString(&this.llvmModule, this.cliOptions); | |
| 538 | 3495 | } | |
| 539 | |||
| 540 | // Dump optimized IR code | ||
| 541 | 3495 | if this.cliOptions.dump.dumpIR { | |
| 542 | 571 | const String optLevelStr = toString(this.cliOptions.optLevel); | |
| 543 | 571 | this.dumpOutput(this.compilerOutput.irOptString, String("Optimized IR Code"), "ir-code-O" + optLevelStr + ".ll"); | |
| 544 | 571 | } | |
| 545 | |||
| 546 | 3495 | this.previousStage = CompileStageType::IR_OPTIMIZER; | |
| 547 | 3495 | this.resourceManager.compileProgress.finishBackEndStage(BackEndStage::IR_OPTIMIZER); | |
| 548 | 3495 | timer.stop(); | |
| 549 | 3495 | this.printStatusMessage("IR Optimizer", CompileStageIOType::IO_IR, CompileStageIOType::IO_IR, this.compilerOutput.times.irOptimizer); | |
| 550 | 3495 | } | |
| 551 | |||
| 552 | 4 | public p SourceFile.runPreLinkIROptimizer() { | |
| 553 | 4 | assert this.cliOptions.useLTO; | |
| 554 | |||
| 555 | // Skip if this stage has already been done | ||
| 556 | 4 | if this.previousStage >= CompileStageType::IR_OPTIMIZER { | |
| 557 | ✗ | return; | |
| 558 | } | ||
| 559 | |||
| 560 | 4 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.irOptimizer); | |
| 561 | 4 | timer.start(); | |
| 562 | 4 | this.resourceManager.compileProgress.startBackEndStage(BackEndStage::IR_OPTIMIZER); | |
| 563 | |||
| 564 | // Optimize this source file | ||
| 565 | 4 | IROptimizer irOptimizer = IROptimizer(this.resourceManager, this); | |
| 566 | 4 | irOptimizer.prepare(); | |
| 567 | 4 | irOptimizer.optimizePreLink(); | |
| 568 | |||
| 569 | // Save the optimized ir string in the compiler output | ||
| 570 | 4 | if this.cliOptions.dump.dumpIR || this.cliOptions.testMode { | |
| 571 | 4 | this.compilerOutput.irOptString = getIRString(&this.llvmModule, this.cliOptions); | |
| 572 | 4 | } | |
| 573 | |||
| 574 | // Dump optimized IR code | ||
| 575 | 4 | if this.cliOptions.dump.dumpIR { | |
| 576 | 2 | this.dumpOutput(this.compilerOutput.irOptString, String("Optimized IR Code (pre-link)"), String("ir-code-lto-pre-link.ll")); | |
| 577 | 2 | } | |
| 578 | |||
| 579 | 4 | this.resourceManager.compileProgress.finishBackEndStage(BackEndStage::IR_OPTIMIZER); | |
| 580 | 4 | timer.pause(); | |
| 581 | 4 | } | |
| 582 | |||
| 583 | 4 | public p SourceFile.runBitcodeLinker() { | |
| 584 | 4 | assert this.cliOptions.useLTO; | |
| 585 | |||
| 586 | // Skip if this is not the main source file | ||
| 587 | 4 | if !this.isMainFile { | |
| 588 | 2 | return; | |
| 589 | } | ||
| 590 | |||
| 591 | // Skip if this stage has already been done | ||
| 592 | 2 | if this.previousStage >= CompileStageType::IR_OPTIMIZER { | |
| 593 | ✗ | return; | |
| 594 | } | ||
| 595 | |||
| 596 | 2 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.irOptimizer); | |
| 597 | 2 | timer.resume(); | |
| 598 | // The bitcode linker and the post-link optimizer form one stage for the whole program | ||
| 599 | 2 | this.resourceManager.compileProgress.startBackEndStage(BackEndStage::LTO_IR_OPTIMIZER); | |
| 600 | |||
| 601 | // Link all source files together | ||
| 602 | 2 | BitcodeLinker linker = BitcodeLinker(this.resourceManager); | |
| 603 | 2 | linker.link(); | |
| 604 | |||
| 605 | 2 | timer.pause(); | |
| 606 | 2 | } | |
| 607 | |||
| 608 | 4 | public p SourceFile.runPostLinkIROptimizer() { | |
| 609 | 4 | assert this.cliOptions.useLTO; | |
| 610 | |||
| 611 | // Skip if this is not the main source file | ||
| 612 | 4 | if !this.isMainFile { | |
| 613 | 2 | return; | |
| 614 | } | ||
| 615 | |||
| 616 | // Skip if this stage has already been done | ||
| 617 | 2 | if this.previousStage >= CompileStageType::IR_OPTIMIZER { | |
| 618 | ✗ | return; | |
| 619 | } | ||
| 620 | |||
| 621 | 2 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.irOptimizer); | |
| 622 | 2 | timer.resume(); | |
| 623 | |||
| 624 | // Optimize LTO module | ||
| 625 | 2 | IROptimizer irOptimizer = IROptimizer(this.resourceManager, this); | |
| 626 | 2 | irOptimizer.prepare(); | |
| 627 | 2 | irOptimizer.optimizePostLink(); | |
| 628 | |||
| 629 | // Save the optimized ir string in the compiler output | ||
| 630 | 2 | if this.cliOptions.dump.dumpIR || this.cliOptions.testMode { | |
| 631 | 2 | this.compilerOutput.irOptString = getIRString(&this.resourceManager.ltoModule, this.cliOptions); | |
| 632 | 2 | } | |
| 633 | |||
| 634 | // Dump optimized IR code | ||
| 635 | 2 | if this.cliOptions.dump.dumpIR { | |
| 636 | 1 | this.dumpOutput(this.compilerOutput.irOptString, String("Optimized IR Code (post-Link)"), String("ir-code-lto-post-link.ll")); | |
| 637 | 1 | } | |
| 638 | |||
| 639 | 2 | this.previousStage = CompileStageType::IR_OPTIMIZER; | |
| 640 | 2 | this.resourceManager.compileProgress.finishBackEndStage(BackEndStage::LTO_IR_OPTIMIZER); | |
| 641 | 2 | timer.stop(); | |
| 642 | 2 | this.printStatusMessage("IR Optimizer", CompileStageIOType::IO_IR, CompileStageIOType::IO_IR, this.compilerOutput.times.irOptimizer); | |
| 643 | 2 | } | |
| 644 | |||
| 645 | 3499 | public p SourceFile.runObjectEmitter() { | |
| 646 | // Skip if this stage has already been done | ||
| 647 | 3499 | if this.previousStage >= CompileStageType::OBJECT_EMITTER { | |
| 648 | ✗ | return; | |
| 649 | } | ||
| 650 | |||
| 651 | // Skip if LTO is enabled and this is not the main source file | ||
| 652 | 3499 | if this.cliOptions.useLTO && !this.isMainFile { | |
| 653 | 2 | return; | |
| 654 | } | ||
| 655 | |||
| 656 | 3497 | Timer timer = Timer(TimerMode::MILLIS, &this.compilerOutput.times.objectEmitter); | |
| 657 | 3497 | timer.start(); | |
| 658 | // With LTO, the main source file emits the object file for the whole program | ||
| 659 | 3497 | const BackEndStage progressStage = this.cliOptions.useLTO ? BackEndStage::LTO_OBJECT_EMITTER : BackEndStage::OBJECT_EMITTER; | |
| 660 | 3497 | this.resourceManager.compileProgress.startBackEndStage(progressStage); | |
| 661 | |||
| 662 | // The code generation opt level matches the opt level, the IR was generated and optimized with. The LLVM C API cannot | ||
| 663 | // change it afterwards, so the target machine is created with it in the constructor. | ||
| 664 | |||
| 665 | // Deduce an object file path | ||
| 666 | 3497 | this.objectFilePath = this.getObjectFilePath(); | |
| 667 | |||
| 668 | // Pick a concrete emitter based on the selected backend. The driver only accepts the TPDE backend, if the compiler is backed | ||
| 669 | // by TPDE | ||
| 670 | 3497 | LLVMObjectEmitter llvmObjectEmitter = LLVMObjectEmitter(this.resourceManager, this); | |
| 671 | 3497 | TPDEObjectEmitter tpdeObjectEmitter = TPDEObjectEmitter(this.cliOptions.useLTO ? &this.resourceManager.ltoModule : &this.llvmModule); | |
| 672 | 3497 | IAbstractObjectEmitter* objectEmitter = &llvmObjectEmitter; | |
| 673 | 3497 | if this.cliOptions.backend == Backend::TPDE { | |
| 674 | 2 | objectEmitter = &tpdeObjectEmitter; | |
| 675 | 2 | } | |
| 676 | |||
| 677 | // Emit object for this source file | ||
| 678 | 3497 | objectEmitter.emit(this.objectFilePath); | |
| 679 | |||
| 680 | // Save assembly string in the compiler output (TPDE emits a placeholder note) | ||
| 681 | 3497 | if this.cliOptions.isNativeTarget && (this.cliOptions.dump.dumpAssembly || this.cliOptions.testMode) { | |
| 682 | 3385 | objectEmitter.getASMString(this.compilerOutput.asmString); | |
| 683 | 3385 | } | |
| 684 | |||
| 685 | // Dump assembly code | ||
| 686 | 3497 | if this.cliOptions.dump.dumpAssembly { | |
| 687 | ✗ | this.dumpOutput(this.compilerOutput.asmString, String("Assembly code"), String("assembly-code.s")); | |
| 688 | ✗ | } | |
| 689 | |||
| 690 | // The object file is registered with the linker in concludeCompilation and not here, because this stage may run on a | ||
| 691 | // worker thread of the parallel back end and the linker input order has to stay deterministic. | ||
| 692 | |||
| 693 | 3497 | this.previousStage = CompileStageType::OBJECT_EMITTER; | |
| 694 | 3497 | this.resourceManager.compileProgress.finishBackEndStage(progressStage); | |
| 695 | 3497 | timer.stop(); | |
| 696 | 3497 | this.printStatusMessage("Object Emitter", CompileStageIOType::IO_IR, CompileStageIOType::IO_OBJECT_FILE, this.compilerOutput.times.objectEmitter); | |
| 697 | 3497 | } | |
| 698 | |||
| 699 | 3499 | public p SourceFile.concludeCompilation() { | |
| 700 | // Skip if this stage has already been done | ||
| 701 | 3499 | if this.previousStage >= CompileStageType::FINISHED { | |
| 702 | ✗ | return; | |
| 703 | } | ||
| 704 | |||
| 705 | // Add the emitted object file to the linker objects. This happens here and not in runObjectEmitter, because | ||
| 706 | // concludeCompilation is always driven serially and in dependency order, while the object emitter may run on a worker | ||
| 707 | // thread of the parallel back end. | ||
| 708 | 3499 | if !this.objectFilePath.isEmpty() { | |
| 709 | 3497 | this.resourceManager.linker.addEmittedObjectFile(this.objectFilePath); | |
| 710 | 3497 | } | |
| 711 | |||
| 712 | // Save type registry as string in the compiler output | ||
| 713 | 3499 | if this.isMainFile && (this.cliOptions.dump.dumpTypes || this.cliOptions.testMode) { | |
| 714 | 760 | this.compilerOutput.typesString = this.resourceManager.typeRegistry.dump(); | |
| 715 | 760 | } | |
| 716 | |||
| 717 | // Dump type registry | ||
| 718 | 3499 | if this.isMainFile && this.cliOptions.dump.dumpTypes { | |
| 719 | 6 | this.dumpOutput(this.compilerOutput.typesString, String("Type Registry"), String("type-registry.out")); | |
| 720 | 6 | } | |
| 721 | |||
| 722 | // Save cache statistics as string in the compiler output | ||
| 723 | 3499 | if this.isMainFile && (this.cliOptions.dump.dumpCacheStats || this.cliOptions.testMode) { | |
| 724 | 760 | this.dumpCacheStats(); | |
| 725 | 760 | } | |
| 726 | |||
| 727 | // Dump lookup cache statistics | ||
| 728 | 3499 | if this.isMainFile && this.cliOptions.dump.dumpCacheStats { | |
| 729 | 1 | this.dumpOutput(this.compilerOutput.cacheStats, String("Cache Statistics"), String("cache-stats.out")); | |
| 730 | 1 | } | |
| 731 | |||
| 732 | 3499 | if this.cliOptions.printDebugOutput { | |
| 733 | ✗ | println("Finished compiling " + this.fileName); | |
| 734 | ✗ | } | |
| 735 | |||
| 736 | 3499 | this.previousStage = CompileStageType::FINISHED; | |
| 737 | 3499 | } | |
| 738 | |||
| 739 | 5007 | public p SourceFile.runFrontEnd() { | |
| 740 | // The front end of the main source file covers the front ends of all its (transitive) dependencies. A circular import | ||
| 741 | // re-enters the front end of the main source file, so only measure the outermost call, which starts with its parser | ||
| 742 | 5007 | const bool measureWallTime = this.isMainFile && this.previousStage == CompileStageType::NONE; | |
| 743 | 5007 | if measureWallTime { | |
| 744 | 1052 | this.resourceManager.frontEndTimer.start(); | |
| 745 | 1052 | } | |
| 746 | |||
| 747 | 5007 | this.runParser(); | |
| 748 | 5007 | if this.resourceManager.abortCompilation { return; } | |
| 749 | 5007 | this.runASTVisualizer(); | |
| 750 | 5007 | if this.resourceManager.abortCompilation { return; } | |
| 751 | 5007 | this.runImportCollector(); | |
| 752 | 5007 | if this.resourceManager.abortCompilation { return; } | |
| 753 | 5007 | this.runSymbolTableBuilder(); | |
| 754 | 5007 | if this.resourceManager.abortCompilation { return; } | |
| 755 | |||
| 756 | 5007 | if measureWallTime { | |
| 757 | 1009 | this.resourceManager.frontEndTimer.stop(); | |
| 758 | 1009 | } | |
| 759 | 5007 | } | |
| 760 | |||
| 761 | 1255 | public p SourceFile.runMiddleEnd() { | |
| 762 | 1255 | if this.isMainFile { | |
| 763 | 1009 | this.resourceManager.middleEndTimer.start(); | |
| 764 | 1009 | } | |
| 765 | |||
| 766 | // Merge the exported name registries of all (transitive) dependencies into the respective importing files. This is | ||
| 767 | // the deferred tail of the front-end: it must run after every reachable file has built its own registry, which is | ||
| 768 | // why it cannot live inside the per-file front-end recursion (a circular import would otherwise merge a dependency | ||
| 769 | // whose registry is not populated yet). runMiddleEnd is the first stage that is only ever invoked at the top level. | ||
| 770 | 1255 | this.mergeNameRegistriesRecursive(); | |
| 771 | 1255 | if this.resourceManager.abortCompilation { return; } | |
| 772 | // From here on, struct manifestations may be substantiated, and each of them gets its compiler-generated default | ||
| 773 | // members decided right at that point (see TypeChecker.createImplicitDefaultMembers). Once the middle end is done, | ||
| 774 | // every manifestation exists and was decided on, so the back end must not create any more of them. | ||
| 775 | 763 | DefaultMemberCreationSection _; | |
| 776 | // We need two runs here due to generics. | ||
| 777 | // The first run to determine all concrete function/struct/interface substantiations | ||
| 778 | 763 | this.runTypeCheckerPre(); // Visit the dependency tree from bottom to top | |
| 779 | 763 | if this.resourceManager.abortCompilation { return; } | |
| 780 | // The second run to ensure, also generic scopes are type-checked properly | ||
| 781 | 763 | this.runTypeCheckerPost(); // Visit the dependency tree from top to bottom in topological order | |
| 782 | 763 | if this.resourceManager.abortCompilation { return; } | |
| 783 | // The per-file convergence loop inside runTypeCheckerPost is scoped to its own call stack: a cross-file revisit | ||
| 784 | // request that lands on a file whose loop already unwound (e.g. a recursive generic dtor chain that closes back | ||
| 785 | // through a runtime module which was itself gated behind another, not-yet-checked importer) is otherwise dropped, | ||
| 786 | // leaving a fully-substantiated manifestation whose body was never type-checked. Sweep every source file in the | ||
| 787 | // program for a straggling revisit request and drive it to convergence directly, repeating until none are left. | ||
| 788 | 763 | bool anySourceFileRevisitPending = true; | |
| 789 | 1531 | while anySourceFileRevisitPending { | |
| 790 | 768 | anySourceFileRevisitPending = false; | |
| 791 | // Snapshot the current files before driving any of them: runTypeCheckerPost() below can itself trigger a | ||
| 792 | // freshly-discovered runtime import (SourceFile.requestRuntimeModule -> GlobalResourceManager.createSourceFile), | ||
| 793 | // which inserts into resourceManager.sourceFiles, so a stable list of pointers is collected first. It is taken in | ||
| 794 | // creation order, so that the revisits happen in a deterministic order, independent of the hash function of the map. | ||
| 795 | 768 | Vector<SourceFile*> sourceFilesSnapshot; | |
| 796 | 4342 | foreach SourceFile* createdSourceFile : this.resourceManager.sourceFilesInCreationOrder { | |
| 797 | 3574 | sourceFilesSnapshot.pushBack(createdSourceFile); | |
| 798 | 3574 | } | |
| 799 | 4342 | foreach SourceFile* sourceFile : sourceFilesSnapshot { | |
| 800 | 3574 | if sourceFile.reVisitRequested { | |
| 801 | 2 | sourceFile.runTypeCheckerPost(); | |
| 802 | 2 | anySourceFileRevisitPending = true; | |
| 803 | 2 | } | |
| 804 | 3574 | } | |
| 805 | // A source file created mid-sweep (see above) still needs its own pass; it starts out with reVisitRequested | ||
| 806 | // true, so re-looping picks it up via the snapshot taken on the next iteration. | ||
| 807 | 768 | if this.resourceManager.sourceFiles.getSize() > sourceFilesSnapshot.getSize() { | |
| 808 | ✗ | anySourceFileRevisitPending = true; | |
| 809 | ✗ | } | |
| 810 | 768 | } | |
| 811 | 763 | if this.resourceManager.abortCompilation { return; } | |
| 812 | // Check that no two definitions of the program are exported under the same linker symbol name | ||
| 813 | 763 | this.checkForExportedSymbolCollisions(); | |
| 814 | 763 | if this.resourceManager.abortCompilation { return; } | |
| 815 | // Visualize dependency graph | ||
| 816 | 763 | this.runDependencyGraphVisualizer(); | |
| 817 | 763 | if this.resourceManager.abortCompilation { return; } | |
| 818 | |||
| 819 | 763 | if this.isMainFile { | |
| 820 | 763 | this.resourceManager.middleEndTimer.stop(); | |
| 821 | 763 | } | |
| 822 | 763 | } | |
| 823 | |||
| 824 | 6533 | p SourceFile.collectBackEndSourceFiles(Vector<SourceFile*>& backEndSourceFiles) { | |
| 825 | // Guard against collecting a file that already went through the back end. Circular imports form a cycle in the | ||
| 826 | // dependency graph, so the deps-first recursion below would otherwise loop forever. | ||
| 827 | 6533 | if this.backEndStarted { | |
| 828 | 3034 | return; | |
| 829 | } | ||
| 830 | 3499 | this.backEndStarted = true; | |
| 831 | |||
| 832 | // Collect all dependencies first, so that they end up in front of this file in the resulting list | ||
| 833 | 9272 | foreach dyn dependency : this.dependencies { | |
| 834 | 5773 | SourceFile* sourceFile = dependency.getSecond(); | |
| 835 | 5773 | sourceFile.collectBackEndSourceFiles(backEndSourceFiles); | |
| 836 | 5773 | } | |
| 837 | |||
| 838 | 3499 | backEndSourceFiles.pushBack(this); | |
| 839 | 3499 | } | |
| 840 | |||
| 841 | 3499 | p SourceFile.runBackEndForThisFile() { | |
| 842 | 3499 | this.runIRGenerator(); | |
| 843 | 3499 | if this.resourceManager.abortCompilation { return; } | |
| 844 | 3499 | if this.cliOptions.useLTO { | |
| 845 | 4 | this.runPreLinkIROptimizer(); | |
| 846 | 4 | if this.resourceManager.abortCompilation { return; } | |
| 847 | 4 | this.runBitcodeLinker(); | |
| 848 | 4 | if this.resourceManager.abortCompilation { return; } | |
| 849 | 4 | this.runPostLinkIROptimizer(); | |
| 850 | 4 | if this.resourceManager.abortCompilation { return; } | |
| 851 | 4 | } else { | |
| 852 | 3495 | this.runDefaultIROptimizer(); | |
| 853 | 3495 | if this.resourceManager.abortCompilation { return; } | |
| 854 | 3495 | } | |
| 855 | 3499 | this.runObjectEmitter(); | |
| 856 | 3499 | this.releaseBackEndResources(); | |
| 857 | 3499 | } | |
| 858 | |||
| 859 | /** | ||
| 860 | * Release the LLVM resources of this source file, as soon as its object file was emitted. Releasing them early bounds the peak memory usage to the resources of the source files, whose back end | ||
| 861 | * runs at the moment. Nothing uses them afterwards, because the back end of each source file only works on its own resources. | ||
| 862 | * With LTO, the modules are linked into the LTO module and the main source file emits the object file, so they are kept. | ||
| 863 | */ | ||
| 864 | 3499 | p SourceFile.releaseBackEndResources() { | |
| 865 | 3499 | if this.cliOptions.useLTO { | |
| 866 | 4 | return; | |
| 867 | } | ||
| 868 | // The LLVM types live in the context, so forget them before. The builder and the module have to be disposed before the | ||
| 869 | // context as well | ||
| 870 | 3495 | this.typeToLLVMTypeMapping.clear(); | |
| 871 | 3495 | this.builder.dispose(); | |
| 872 | 3495 | this.llvmModule.dispose(); | |
| 873 | 3495 | this.targetMachine.dispose(); | |
| 874 | 3495 | this.context.dispose(); | |
| 875 | 3495 | } | |
| 876 | |||
| 877 | 760 | public p SourceFile.runBackEnd() { | |
| 878 | 760 | if this.isMainFile { | |
| 879 | 760 | this.resourceManager.backEndTimer.start(); | |
| 880 | 760 | } | |
| 881 | |||
| 882 | // Flatten the dependency graph into the order the back end used to recurse in: every file comes after all of its | ||
| 883 | // dependencies, and files that already ran their back end are skipped. | ||
| 884 | 760 | Vector<SourceFile*> backEndSourceFiles; | |
| 885 | 760 | this.collectBackEndSourceFiles(backEndSourceFiles); | |
| 886 | |||
| 887 | // Nothing to do if this file and all of its dependencies already went through the back end | ||
| 888 | 760 | if backEndSourceFiles.isEmpty() { | |
| 889 | ✗ | return; | |
| 890 | } | ||
| 891 | |||
| 892 | // Unlike the front end and the middle end, the back end has no cross-file data dependencies: every source file owns | ||
| 893 | // its own LLVMContext, IRBuilder, TargetMachine and llvm::Module, and references to symbols of other files are emitted | ||
| 894 | // as declarations into the local module. So the per-file pipelines can simply be spread over a worker pool. | ||
| 895 | // Exceptions, in which the back end stays serial: | ||
| 896 | // - LTO, because all source files share the LTO context and module of the GlobalResourceManager | ||
| 897 | // - dump modes, because they write to the console/output dir and their ordering is part of the user-visible output | ||
| 898 | 760 | const bool dumpRequested = this.cliOptions.dump.dumpIR || this.cliOptions.dump.dumpAssembly || this.cliOptions.dump.dumpObjectFiles; | |
| 899 | 760 | const unsigned long compileJobCount = this.resourceManager.getCompileJobCount(); | |
| 900 | 760 | const unsigned long jobCount = compileJobCount < backEndSourceFiles.getSize() ? compileJobCount : backEndSourceFiles.getSize(); | |
| 901 | 760 | const bool runParallel = jobCount > 1ul && !this.cliOptions.useLTO && !dumpRequested; | |
| 902 | |||
| 903 | 760 | if runParallel { | |
| 904 | 1 | ThreadPool threadPool = ThreadPool(cast<unsigned short>(jobCount)); | |
| 905 | 17 | foreach SourceFile* sourceFile : backEndSourceFiles { | |
| 906 | 32 | threadPool.enqueue(p() [[async]] { | |
| 907 | 16 | sourceFile.runBackEndForThisFile(); | |
| 908 | 16 | }); | |
| 909 | 16 | } | |
| 910 | // Enter the parallel section before the worker threads start and leave it only after all of them were joined. A | ||
| 911 | // failing source file aborts the compilation on its worker thread (see abortWithError), which ends the process. | ||
| 912 | 1 | ParallelSection _; | |
| 913 | 1 | threadPool.start(); | |
| 914 | 1 | threadPool.join(); | |
| 915 | 1 | } else { | |
| 916 | 4242 | foreach SourceFile* sourceFile : backEndSourceFiles { | |
| 917 | 3483 | sourceFile.runBackEndForThisFile(); | |
| 918 | 3483 | if this.resourceManager.abortCompilation { return; } | |
| 919 | 3483 | } | |
| 920 | 759 | } | |
| 921 | 760 | if this.resourceManager.abortCompilation { return; } | |
| 922 | |||
| 923 | // Conclude the compilation of all source files. This registers the emitted object files with the linker, which has to | ||
| 924 | // happen serially and in a fixed order to stay deterministic. | ||
| 925 | 4259 | foreach SourceFile* sourceFile : backEndSourceFiles { | |
| 926 | 3499 | sourceFile.concludeCompilation(); | |
| 927 | 3499 | } | |
| 928 | |||
| 929 | 760 | if this.isMainFile { | |
| 930 | 760 | this.resourceManager.backEndTimer.stop(); | |
| 931 | 760 | if this.cliOptions.printDebugOutput { | |
| 932 | ✗ | this.dumpCompilationStats(); | |
| 933 | ✗ | } | |
| 934 | 760 | } | |
| 935 | 760 | } | |
| 936 | |||
| 937 | 6033 | public p SourceFile.addDependency(SourceFile* sourceFile, const String& dependencyName) { | |
| 938 | // Circular imports are explicitly supported, so cycles are not rejected here. Source files are deduplicated by path | ||
| 939 | // in GlobalResourceManager::createSourceFile, so a cyclic import resolves to the same SourceFile instance and the | ||
| 940 | // pipeline drivers guard against re-entering a file that is already in progress. | ||
| 941 | |||
| 942 | // Add the dependency. Do not demote the compilation root (parent == nullptr) to a non-main file. | ||
| 943 | 6033 | if sourceFile.parent != nil<SourceFile*> { | |
| 944 | 6032 | sourceFile.isMainFile = false; | |
| 945 | 6032 | } | |
| 946 | 6033 | if !this.dependencies.contains(dependencyName) { | |
| 947 | 6033 | this.dependencies.upsert(dependencyName, sourceFile); | |
| 948 | 6033 | } | |
| 949 | |||
| 950 | // Add the dependant | ||
| 951 | 6033 | sourceFile.dependants.pushBack(this); | |
| 952 | 6033 | } | |
| 953 | |||
| 954 | // Shortcuts used by the compiler passes | ||
| 955 | 35729 | public f<Scope*> SourceFile.getGlobalScope() { | |
| 956 | 35729 | return this.globalScope; | |
| 957 | } | ||
| 958 | |||
| 959 | 4056 | public f<const FilePath&> SourceFile.getFilePath() { | |
| 960 | 4056 | return this.filePath; | |
| 961 | } | ||
| 962 | |||
| 963 | 3942 | public f<bool> SourceFile.isMainSourceFile() { | |
| 964 | 3942 | return this.isMainFile; | |
| 965 | } | ||
| 966 | |||
| 967 | // True if the exported name registry already maps the name to the given symbol table entry | ||
| 968 | 45761 | public f<bool> SourceFile.hasNameRegistryTarget(const String& symbolName, const SymbolTableEntry* target) { | |
| 969 | 45761 | const NameRegistryEntry* existing = this.getNameRegistryEntry(symbolName); | |
| 970 | 45761 | return existing != nil<NameRegistryEntry*> && existing.targetEntry == target; | |
| 971 | } | ||
| 972 | |||
| 973 | 1491 | public p SourceFile.setAlwaysKeepSymbolsOnNameCollision(bool value) { this.alwaysKeepSymbolsOnNameCollision = value; } | |
| 974 | 4 | public p SourceFile.setIgnoreWarnings(bool value) { this.ignoreWarnings = value; } | |
| 975 | 4 | public p SourceFile.setExplicitErrorHandling(bool value) { this.explicitErrorHandling = value; } | |
| 976 | 5 | public p SourceFile.setErrorReturnTracing(bool value) { this.errorReturnTracing = value; } | |
| 977 | |||
| 978 | 430538 | public f<bool> SourceFile.imports(const SourceFile* sourceFile) { | |
| 979 | 1506933 | foreach dyn dependency : this.dependencies { | |
| 980 | 1117582 | if dependency.getSecond() == sourceFile { | |
| 981 | 41187 | return true; | |
| 982 | } | ||
| 983 | 1076395 | } | |
| 984 | 389351 | return false; | |
| 985 | } | ||
| 986 | |||
| 987 | 21530 | #[ignoreUnusedReturnValue] | |
| 988 | 21530 | public f<SourceFile*> SourceFile.requestRuntimeModule(RuntimeModule runtimeModule) { | |
| 989 | // Check if the module was already imported | ||
| 990 | 21530 | if this.isRuntimeModuleAvailable(runtimeModule) { | |
| 991 | 18008 | return this.resourceManager.runtimeModuleManager.getModule(runtimeModule); | |
| 992 | } | ||
| 993 | 3522 | return this.resourceManager.runtimeModuleManager.requestModule(this, runtimeModule); | |
| 994 | } | ||
| 995 | |||
| 996 | 22924 | public f<bool> SourceFile.isRuntimeModuleAvailable(RuntimeModule runtimeModule) { | |
| 997 | 22924 | return (this.importedRuntimeModules & runtimeModule) != 0; | |
| 998 | } | ||
| 999 | |||
| 1000 | 166845 | public p SourceFile.addNameRegistryEntry(const String& symbolName, unsigned long typeId, SymbolTableEntry* entry, Scope* scope, bool keepNewOnCollision, SymbolTableEntry* importEntry) { | |
| 1001 | 166845 | if keepNewOnCollision || !this.exportedNameRegistry.contains(symbolName) { // Overwrite potential existing entry | |
| 1002 | 166561 | if this.exportedNameRegistry.contains(symbolName) { | |
| 1003 | 16763 | const SymbolTableEntry* oldTargetEntry = this.exportedNameRegistry.get(symbolName).targetEntry; | |
| 1004 | 16763 | this.untrackNameRegistryTargetEntry(oldTargetEntry); | |
| 1005 | 16763 | this.exportedNameRegistry.remove(symbolName); | |
| 1006 | 16763 | } | |
| 1007 | 166561 | this.exportedNameRegistry.upsert(symbolName, NameRegistryEntry{typeId, entry, scope, importEntry}); | |
| 1008 | 166561 | this.trackNameRegistryTargetEntry(entry); | |
| 1009 | 166561 | if keepNewOnCollision && this.ambiguousNameRegistry.contains(symbolName) { | |
| 1010 | 132 | this.ambiguousNameRegistry.remove(symbolName); | |
| 1011 | 132 | } | |
| 1012 | 166561 | } else { // Name collision => we must remove the existing entry. Remember the colliding imports to report the ambiguity later | |
| 1013 | 284 | if !this.ambiguousNameRegistry.contains(symbolName) { | |
| 1014 | 255 | this.ambiguousNameRegistry.upsert(symbolName, Vector<const SymbolTableEntry*>()); | |
| 1015 | 255 | } | |
| 1016 | 284 | Vector<const SymbolTableEntry*>& collidingImports = this.ambiguousNameRegistry.get(symbolName); | |
| 1017 | 284 | collidingImports.pushBack(this.exportedNameRegistry.get(symbolName).importEntry); | |
| 1018 | 284 | collidingImports.pushBack(importEntry); | |
| 1019 | 284 | const SymbolTableEntry* oldTargetEntry = this.exportedNameRegistry.get(symbolName).targetEntry; | |
| 1020 | 284 | this.untrackNameRegistryTargetEntry(oldTargetEntry); | |
| 1021 | 284 | this.exportedNameRegistry.remove(symbolName); | |
| 1022 | 284 | } | |
| 1023 | 166845 | } | |
| 1024 | |||
| 1025 | 662101 | public f<const NameRegistryEntry*> SourceFile.getNameRegistryEntry(const String& symbolName) { | |
| 1026 | 662101 | if !this.exportedNameRegistry.contains(symbolName) { | |
| 1027 | 297474 | return nil<NameRegistryEntry*>; | |
| 1028 | } | ||
| 1029 | |||
| 1030 | // Resolve registry entry for the given name | ||
| 1031 | 364627 | const NameRegistryEntry* entry = &this.exportedNameRegistry.get(symbolName); | |
| 1032 | |||
| 1033 | // Mark the import entry as used | ||
| 1034 | 364627 | if entry.importEntry != nil<SymbolTableEntry*> { | |
| 1035 | 20597 | entry.importEntry.used = true; | |
| 1036 | 20597 | } | |
| 1037 | |||
| 1038 | 364627 | return entry; | |
| 1039 | } | ||
| 1040 | |||
| 1041 | 19 | public f<bool> SourceFile.isAmbiguousName(const String& symbolName) { | |
| 1042 | 19 | return this.ambiguousNameRegistry.contains(symbolName); | |
| 1043 | } | ||
| 1044 | |||
| 1045 | /** | ||
| 1046 | * Check if the name registry of this source file knows the struct manifestation with the given body scope | ||
| 1047 | * | ||
| 1048 | * @param bodyScope Body scope of the struct manifestation | ||
| 1049 | * @param structName Name of the struct | ||
| 1050 | * @return Known or not | ||
| 1051 | */ | ||
| 1052 | 95999 | public f<bool> SourceFile.isStructKnownByNameRegistry(Scope* bodyScope, const String& structName) { | |
| 1053 | 95999 | if bodyScope == nil<Scope*> || bodyScope.parent == nil<Scope*> { | |
| 1054 | ✗ | return false; | |
| 1055 | } | ||
| 1056 | |||
| 1057 | // All manifestations of a struct live in the scope the struct is defined in, next to the struct's symbol table entry. | ||
| 1058 | // The name registry entry of a struct points to this symbol table entry, so we can look it up instead of scanning the | ||
| 1059 | // whole name registry | ||
| 1060 | 95999 | const SymbolTableEntry* structEntry = bodyScope.parent.lookupStrict(structName); | |
| 1061 | 95999 | if structEntry == nil<const SymbolTableEntry*> || !this.nameRegistryTargetEntryCounts.contains(getEntryAddress(structEntry)) { | |
| 1062 | 7126 | return false; | |
| 1063 | } | ||
| 1064 | 88873 | const QualType& structType = structEntry.getQualType(); | |
| 1065 | 88873 | if !structType.isBase(SuperType::TY_STRUCT) { | |
| 1066 | ✗ | return false; | |
| 1067 | } | ||
| 1068 | |||
| 1069 | // Check if the body scope belongs to one of the manifestations of the struct | ||
| 1070 | 88873 | ASTNode* declNode = structEntry.declNode; | |
| 1071 | 88873 | if declNode.kind != ASTNodeKind::STRUCT_DEF { | |
| 1072 | ✗ | return false; | |
| 1073 | } | ||
| 1074 | 88873 | const ASTStructDefNode* structDefNode = downcast<ASTStructDefNode>(declNode); | |
| 1075 | 141282 | foreach Struct* manifestation : structDefNode.structManifestations { | |
| 1076 | 141276 | if manifestation.scope == bodyScope { | |
| 1077 | 88867 | return true; | |
| 1078 | } | ||
| 1079 | 52409 | } | |
| 1080 | 6 | return false; | |
| 1081 | } | ||
| 1082 | |||
| 1083 | /** | ||
| 1084 | * Count a new name registry entry pointing to the given target entry | ||
| 1085 | * | ||
| 1086 | * @param targetEntry Target entry of the name registry entry | ||
| 1087 | */ | ||
| 1088 | 268126 | p SourceFile.trackNameRegistryTargetEntry(const SymbolTableEntry* targetEntry) { | |
| 1089 | 268126 | if targetEntry == nil<const SymbolTableEntry*> { | |
| 1090 | ✗ | return; | |
| 1091 | } | ||
| 1092 | 268126 | const long entryAddress = getEntryAddress(targetEntry); | |
| 1093 | 268126 | if this.nameRegistryTargetEntryCounts.contains(entryAddress) { | |
| 1094 | 102931 | unsigned long& count = this.nameRegistryTargetEntryCounts.get(entryAddress); | |
| 1095 | 102931 | count++; | |
| 1096 | 102931 | } else { | |
| 1097 | 165195 | this.nameRegistryTargetEntryCounts.upsert(entryAddress, 1ul); | |
| 1098 | 165195 | } | |
| 1099 | 268126 | } | |
| 1100 | |||
| 1101 | /** | ||
| 1102 | * Uncount a removed name registry entry pointing to the given target entry | ||
| 1103 | * | ||
| 1104 | * @param targetEntry Target entry of the name registry entry | ||
| 1105 | */ | ||
| 1106 | 17047 | p SourceFile.untrackNameRegistryTargetEntry(const SymbolTableEntry* targetEntry) { | |
| 1107 | 17047 | if targetEntry == nil<const SymbolTableEntry*> { | |
| 1108 | ✗ | return; | |
| 1109 | } | ||
| 1110 | 17047 | const long entryAddress = getEntryAddress(targetEntry); | |
| 1111 | 17047 | assert this.nameRegistryTargetEntryCounts.contains(entryAddress); | |
| 1112 | 17047 | unsigned long& count = this.nameRegistryTargetEntryCounts.get(entryAddress); | |
| 1113 | 17047 | count--; | |
| 1114 | 17047 | if count == 0ul { | |
| 1115 | 11230 | this.nameRegistryTargetEntryCounts.remove(entryAddress); | |
| 1116 | 11230 | } | |
| 1117 | 17047 | } | |
| 1118 | |||
| 1119 | /** | ||
| 1120 | * Get the address of the given symbol table entry, to use it as a map key | ||
| 1121 | * | ||
| 1122 | * @param entry Symbol table entry | ||
| 1123 | * @return Address of the entry | ||
| 1124 | */ | ||
| 1125 | 381172 | f<long> getEntryAddress(const SymbolTableEntry* entry) { | |
| 1126 | unsafe { | ||
| 1127 | 381172 | return cast<long>(entry); | |
| 1128 | } | ||
| 1129 | } | ||
| 1130 | |||
| 1131 | /** | ||
| 1132 | * Build an error message for a name that is not available, because multiple imports expose it. The message lists the | ||
| 1133 | * colliding imports and suggests how to qualify the name. | ||
| 1134 | * | ||
| 1135 | * @param symbolName Ambiguous name | ||
| 1136 | * @return Error message | ||
| 1137 | */ | ||
| 1138 | 5 | public f<String> SourceFile.getAmbiguousNameMessage(const String& symbolName) { | |
| 1139 | 5 | assert this.isAmbiguousName(symbolName); | |
| 1140 | |||
| 1141 | // Collect the distinct imports that expose the name. Runtime imports have no import entry and are skipped | ||
| 1142 | 5 | Vector<ASTImportDefNode*> importNodes; | |
| 1143 | 5 | const Vector<const SymbolTableEntry*>& importEntries = this.ambiguousNameRegistry.get(symbolName); | |
| 1144 | 17 | for unsigned long i = 0l; i < importEntries.getSize(); i++ { | |
| 1145 | 12 | const SymbolTableEntry* importEntry = importEntries.get(i); | |
| 1146 | 12 | if importEntry == nil<const SymbolTableEntry*> { | |
| 1147 | ✗ | continue; | |
| 1148 | } | ||
| 1149 | 12 | const ASTImportDefNode* importNode = downcast<ASTImportDefNode>(importEntry.declNode); | |
| 1150 | 12 | bool alreadyCollected = false; | |
| 1151 | 22 | for unsigned long j = 0l; j < importNodes.getSize(); j++ { | |
| 1152 | 10 | if importNodes.get(j) == importNode { | |
| 1153 | ✗ | alreadyCollected = true; | |
| 1154 | ✗ | } | |
| 1155 | 10 | } | |
| 1156 | 12 | if !alreadyCollected { | |
| 1157 | 12 | importNodes.pushBack(importNode); | |
| 1158 | 12 | } | |
| 1159 | 12 | } | |
| 1160 | |||
| 1161 | 5 | String msg = String("'") + symbolName + "' is exposed by "; | |
| 1162 | 5 | if importNodes.getSize() < 2l { | |
| 1163 | ✗ | msg += "multiple imports"; | |
| 1164 | ✗ | } else { | |
| 1165 | 5 | if importNodes.getSize() == 2l { | |
| 1166 | 4 | msg += "both "; | |
| 1167 | 4 | } | |
| 1168 | 17 | for unsigned long i = 0l; i < importNodes.getSize(); i++ { | |
| 1169 | 12 | if i > 0l { | |
| 1170 | 7 | msg += i == importNodes.getSize() - 1l ? " and " : ", "; | |
| 1171 | 7 | } | |
| 1172 | 12 | msg += String("'") + importNodes.get(i).importPath + "'"; | |
| 1173 | 12 | } | |
| 1174 | 5 | } | |
| 1175 | |||
| 1176 | // Suggest qualifying the name with the first import name, that can be written in source code | ||
| 1177 | 7 | for unsigned long i = 0l; i < importNodes.getSize(); i++ { | |
| 1178 | 6 | String& importName = importNodes.get(i).importName; | |
| 1179 | 6 | if this.isQualifiableImportName(importName) { | |
| 1180 | 4 | msg += String(". Please qualify it, e.g. '") + importName + SCOPE_ACCESS_TOKEN + symbolName + "'"; | |
| 1181 | 8 | return msg; | |
| 1182 | 4 | } | |
| 1183 | 2 | } | |
| 1184 | 1 | msg += String(". Please import one of them with an alias and qualify it, e.g. 'alias") + SCOPE_ACCESS_TOKEN + symbolName + "'"; | |
| 1185 | 2 | return msg; | |
| 1186 | 5 | } | |
| 1187 | |||
| 1188 | /** | ||
| 1189 | * Check if the given import name can be used to qualify a name in source code, i.e. if it is a valid identifier. | ||
| 1190 | * | ||
| 1191 | * @param importName Import name | ||
| 1192 | * @return Qualifiable or not | ||
| 1193 | */ | ||
| 1194 | 6 | f<bool> SourceFile.isQualifiableImportName(String& importName) { | |
| 1195 | 6 | if importName.isEmpty() { | |
| 1196 | ✗ | return false; | |
| 1197 | } | ||
| 1198 | 25 | for unsigned long i = 0l; i < importName.getLength(); i++ { | |
| 1199 | 21 | const char c = importName[i]; | |
| 1200 | 21 | const bool isLowerLetter = c >= 'a' && c <= 'z'; | |
| 1201 | 21 | const bool isIdentifierChar = isLowerLetter || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; | |
| 1202 | 21 | if !isIdentifierChar || (i == 0l && !isLowerLetter && c != '_') { | |
| 1203 | 2 | return false; | |
| 1204 | } | ||
| 1205 | 19 | } | |
| 1206 | 4 | return true; | |
| 1207 | } | ||
| 1208 | |||
| 1209 | /** | ||
| 1210 | * Abort the compilation if a hard error was raised. Hard errors are recorded (see ErrorManager), so each stage checks for them | ||
| 1211 | * afterwards. | ||
| 1212 | */ | ||
| 1213 | 18183 | public p SourceFile.checkForHardError() { | |
| 1214 | 18183 | const ErrorManager& errorManager = this.resourceManager.errorManager; | |
| 1215 | 18183 | if errorManager.hasHardError { | |
| 1216 | 84 | abortWithError(errorManager.hardErrorMessage); | |
| 1217 | 84 | } | |
| 1218 | 18183 | } | |
| 1219 | |||
| 1220 | /** | ||
| 1221 | * Check that no two definitions of the program are exported under the same linker symbol name. Non-public symbols get | ||
| 1222 | * internal linkage and therefore can never collide. Public ones get external linkage under a name that does not contain | ||
| 1223 | * the source file, so e.g. two public functions with the same signature in different source files would break the linking. | ||
| 1224 | * Every collision is reported as soft error. | ||
| 1225 | */ | ||
| 1226 | 766 | p SourceFile.checkForExportedSymbolCollisions() { | |
| 1227 | 766 | UnorderedMap<String, ASTNode*> exportedSymbols; | |
| 1228 | // Spice cannot hash pointers, so the visited files are identified by their file path, which is unique per source file | ||
| 1229 | 766 | UnorderedSet<String> visitedSourceFiles; | |
| 1230 | 766 | this.registerExportedSymbols(exportedSymbols, visitedSourceFiles); | |
| 1231 | 766 | this.checkForSoftErrors(); | |
| 1232 | 766 | } | |
| 1233 | |||
| 1234 | /** | ||
| 1235 | * Register the exported symbols of this source file and all its (transitive) dependencies in the given registry. This | ||
| 1236 | * covers the same source files as collectBackEndSourceFiles, so exactly the ones that end up in the linked program. | ||
| 1237 | * | ||
| 1238 | * @param exportedSymbols Registry, that maps each linker symbol name to the AST node of its definition | ||
| 1239 | * @param visitedSourceFiles File paths of the source files, that were already registered | ||
| 1240 | */ | ||
| 1241 | 6582 | p SourceFile.registerExportedSymbols(UnorderedMap<String, ASTNode*>& exportedSymbols, UnorderedSet<String>& visitedSourceFiles) { | |
| 1242 | // Guard against circular imports | ||
| 1243 | 6582 | const String filePathStr = String(this.filePath.toString()); | |
| 1244 | 6582 | if visitedSourceFiles.contains(filePathStr) { | |
| 1245 | 3058 | return; | |
| 1246 | } | ||
| 1247 | 3524 | visitedSourceFiles.insert(filePathStr); | |
| 1248 | |||
| 1249 | // Register the symbols of all dependencies first, so that a collision is reported at the definition in the importing file | ||
| 1250 | 9340 | foreach dyn dependency : this.dependencies { | |
| 1251 | 5816 | SourceFile* sourceFile = dependency.getSecond(); | |
| 1252 | 5816 | sourceFile.registerExportedSymbols(exportedSymbols, visitedSourceFiles); | |
| 1253 | 5816 | } | |
| 1254 | |||
| 1255 | 64156 | foreach IVisitable* topLevelDef : this.ast.topLevelDefs { | |
| 1256 | 60632 | ASTNode* node = getASTNode(topLevelDef); | |
| 1257 | 60632 | if node.kind == ASTNodeKind::GLOBAL_VAR_DEF { | |
| 1258 | // Public globals are exported under their plain name | ||
| 1259 | 4208 | const ASTGlobalVarDefNode* globalVarDef = downcast<ASTGlobalVarDefNode>(node); | |
| 1260 | 4208 | const QualType& globalVarType = globalVarDef.entry.getQualType(); | |
| 1261 | 4208 | if globalVarType.isPublic() { | |
| 1262 | 2151 | this.registerExportedSymbol(exportedSymbols, globalVarDef.varName, globalVarDef.varName, node); | |
| 1263 | 2151 | } | |
| 1264 | 60632 | } else if node.kind == ASTNodeKind::FCT_DEF || node.kind == ASTNodeKind::PROC_DEF { | |
| 1265 | 43812 | ASTFctDefBaseNode* fctDef = nil<ASTFctDefBaseNode*>; | |
| 1266 | 43812 | if node.kind == ASTNodeKind::FCT_DEF { | |
| 1267 | 26991 | ASTFctDefNode* fctDefNode = downcastMut<ASTFctDefNode>(node); | |
| 1268 | 26991 | fctDef = &fctDefNode.node; | |
| 1269 | 26991 | } else { | |
| 1270 | 16821 | ASTProcDefNode* procDefNode = downcastMut<ASTProcDefNode>(node); | |
| 1271 | 16821 | fctDef = &procDefNode.node; | |
| 1272 | 16821 | } | |
| 1273 | // Each emitted manifestation of an exported function is exported under its mangled name | ||
| 1274 | 102285 | foreach const model::Function* manifestation : fctDef.manifestations { | |
| 1275 | 58473 | if manifestation.isFullySubstantiated() && isExportedFunction(manifestation, this.cliOptions.generateTestMain) { | |
| 1276 | 38154 | this.registerExportedSymbol(exportedSymbols, manifestation.getMangledName(), manifestation.getSignature(), node); | |
| 1277 | 38154 | } | |
| 1278 | 58473 | } | |
| 1279 | 56424 | } | |
| 1280 | 60632 | } | |
| 1281 | 6582 | } | |
| 1282 | |||
| 1283 | /** | ||
| 1284 | * Register a single exported symbol and report a soft error if another definition is already exported under its name | ||
| 1285 | * | ||
| 1286 | * @param exportedSymbols Registry, that maps each linker symbol name to the AST node of its definition | ||
| 1287 | * @param linkerName Name of the symbol for the linker | ||
| 1288 | * @param displayName Name of the symbol for the error message | ||
| 1289 | * @param declNode Definition of the symbol | ||
| 1290 | */ | ||
| 1291 | 40305 | p SourceFile.registerExportedSymbol(UnorderedMap<String, ASTNode*>& exportedSymbols, const String& linkerName, const String& displayName, ASTNode* declNode) { | |
| 1292 | 40305 | if !exportedSymbols.contains(linkerName) { | |
| 1293 | 40300 | exportedSymbols.upsert(linkerName, declNode); | |
| 1294 | 40300 | return; | |
| 1295 | } | ||
| 1296 | 5 | const ASTNode* existingDeclNode = exportedSymbols.get(linkerName); | |
| 1297 | 5 | if existingDeclNode == declNode { | |
| 1298 | ✗ | return; | |
| 1299 | } | ||
| 1300 | 5 | const String message = "Exported symbol '" + displayName + "' collides with the exported symbol defined at " + existingDeclNode.codeLoc.toPrettyString() + ", because both have the linker name '" + linkerName + "'"; | |
| 1301 | 5 | this.resourceManager.errorManager.addSoftError(declNode, SemanticErrorType::EXPORTED_SYMBOL_COLLISION, message); | |
| 1302 | 5 | } | |
| 1303 | |||
| 1304 | 4641 | public p SourceFile.checkForSoftErrors() { | |
| 1305 | // Check if there are any soft errors and if so, print them | ||
| 1306 | 4641 | if !this.resourceManager.errorManager.softErrors.isEmpty() { | |
| 1307 | 181 | String errorMessage = String("There are unresolved errors. Please fix them and recompile."); | |
| 1308 | 443 | foreach const SoftError& error : this.resourceManager.errorManager.softErrors { | |
| 1309 | 262 | errorMessage += "\n\n"; | |
| 1310 | 262 | errorMessage += error.message; | |
| 1311 | 262 | } | |
| 1312 | 181 | const CompilerError compilerError = CompilerError(CompilerErrorType::UNRESOLVED_SOFT_ERRORS, errorMessage.getRaw()); | |
| 1313 | 181 | abortWithError(compilerError.getMessage()); | |
| 1314 | 181 | } | |
| 1315 | 4641 | } | |
| 1316 | |||
| 1317 | 152 | public f<bool> SourceFile.isLibraryOutput() { | |
| 1318 | 152 | return this.cliOptions.outputContainer == OutputContainer::STATIC_LIBRARY || this.cliOptions.outputContainer == OutputContainer::SHARED_LIBRARY; | |
| 1319 | } | ||
| 1320 | |||
| 1321 | /** | ||
| 1322 | * Get the path of the object file, that the object emitter emits for this source file. With the object file output container, | ||
| 1323 | * the object of the main source file is the output file, so it goes to the output path. | ||
| 1324 | * | ||
| 1325 | * @return Object file path | ||
| 1326 | */ | ||
| 1327 | 3501 | public f<FilePath> SourceFile.getObjectFilePath() { | |
| 1328 | 3501 | if this.isMainFile && this.cliOptions.outputContainer == OutputContainer::OBJECT_FILE { | |
| 1329 | 299 | return this.cliOptions.outputPath; | |
| 1330 | } | ||
| 1331 | 3202 | const string objectFileExt = getOutputFileExtension(this.cliOptions, OutputContainer::OBJECT_FILE); | |
| 1332 | 6404 | return this.cliOptions.outputDir / (this.objectFileStem + "." + objectFileExt); | |
| 1333 | 3202 | } | |
| 1334 | |||
| 1335 | 857 | public p SourceFile.collectAndPrintWarnings() { | |
| 1336 | // Skip if already visited. This keeps circular imports from recursing infinitely, since the dependency graph may contain | ||
| 1337 | // cycles. | ||
| 1338 | 857 | if this.warningsCollected { | |
| 1339 | 20 | return; | |
| 1340 | } | ||
| 1341 | 837 | this.warningsCollected = true; | |
| 1342 | // Print warnings for all dependencies | ||
| 1343 | 1593 | foreach dyn dependency : this.dependencies { | |
| 1344 | 756 | SourceFile* sourceFile = dependency.getSecond(); | |
| 1345 | 756 | if !sourceFile.isStdFile { | |
| 1346 | 97 | sourceFile.collectAndPrintWarnings(); | |
| 1347 | 97 | } | |
| 1348 | 756 | } | |
| 1349 | // Collect warnings for this file | ||
| 1350 | 837 | if !this.ignoreWarnings { | |
| 1351 | 837 | this.globalScope.collectWarnings(this.compilerOutput.warnings); | |
| 1352 | 837 | } | |
| 1353 | // Print warnings for this file. Generic code is checked once per manifestation, so the same warning may have been added | ||
| 1354 | // multiple times. Print each of them only once | ||
| 1355 | 837 | UnorderedSet<String> printedWarnings; | |
| 1356 | 1090 | foreach dyn warning : this.compilerOutput.warnings { | |
| 1357 | 253 | if !printedWarnings.contains(warning.warningMessage) { | |
| 1358 | 242 | printedWarnings.insert(warning.warningMessage); | |
| 1359 | 242 | warning.print(); | |
| 1360 | 242 | } | |
| 1361 | 253 | } | |
| 1362 | 837 | } | |
| 1363 | |||
| 1364 | 3 | public p SourceFile.collectAndPrintLintFindings() { | |
| 1365 | // Skip if already visited. This keeps circular imports from recursing infinitely, since the dependency graph may contain | ||
| 1366 | // cycles. | ||
| 1367 | 3 | if this.lintFindingsCollected { | |
| 1368 | ✗ | return; | |
| 1369 | } | ||
| 1370 | 3 | this.lintFindingsCollected = true; | |
| 1371 | // Collect lint findings for all dependencies | ||
| 1372 | 4 | foreach dyn dependency : this.dependencies { | |
| 1373 | 1 | SourceFile* sourceFile = dependency.getSecond(); | |
| 1374 | 1 | if !sourceFile.isStdFile { | |
| 1375 | ✗ | sourceFile.collectAndPrintLintFindings(); | |
| 1376 | ✗ | } | |
| 1377 | 1 | } | |
| 1378 | // Collect lint findings for this file | ||
| 1379 | 3 | if !this.ignoreWarnings { | |
| 1380 | 3 | LintPass lintPass = LintPass(this.resourceManager, this); | |
| 1381 | 3 | this.compilerOutput.lintFindings = lintPass.lint(this.ast); | |
| 1382 | 3 | } | |
| 1383 | // Print lint findings for this file | ||
| 1384 | 11 | foreach dyn finding : this.compilerOutput.lintFindings { | |
| 1385 | 8 | finding.print(); | |
| 1386 | 8 | } | |
| 1387 | 3 | } | |
| 1388 | |||
| 1389 | ✗ | public f<const SourceFile*> SourceFile.getRootSourceFile() { | |
| 1390 | ✗ | return this.isMainFile ? this : this.parent.getRootSourceFile(); | |
| 1391 | } | ||
| 1392 | |||
| 1393 | /** | ||
| 1394 | * Get the LLVM context, the LLVM entities of this source file live in | ||
| 1395 | * | ||
| 1396 | * @return LLVM context | ||
| 1397 | */ | ||
| 1398 | 40613 | public f<llvm::LLVMContext&> SourceFile.getLLVMContext() { | |
| 1399 | 40613 | return this.cliOptions.useLTO ? this.resourceManager.ltoContext : this.context; | |
| 1400 | } | ||
| 1401 | |||
| 1402 | /** | ||
| 1403 | * Get the LLVM type of the given type. The LLVM types are cached, so that each type gets lowered only once (e.g. named struct | ||
| 1404 | * types would otherwise be created multiple times). | ||
| 1405 | * | ||
| 1406 | * @param rawType Type | ||
| 1407 | * @return LLVM type | ||
| 1408 | */ | ||
| 1409 | 1704919 | public f<llvm::Type> SourceFile.getLLVMType(const symboltable::Type* rawType) { | |
| 1410 | // Check if the type is already in the mapping | ||
| 1411 | 1704919 | long typeAddress = 0l; | |
| 1412 | unsafe { | ||
| 1413 | 1704919 | typeAddress = cast<long>(rawType); | |
| 1414 | } | ||
| 1415 | 1704919 | if this.typeToLLVMTypeMapping.contains(typeAddress) { | |
| 1416 | 1667829 | return this.typeToLLVMTypeMapping.get(typeAddress); | |
| 1417 | } | ||
| 1418 | |||
| 1419 | // If not, generate the LLVM type | ||
| 1420 | 37090 | const llvm::Type llvmType = rawType.toLLVMType(this); | |
| 1421 | 37090 | this.typeToLLVMTypeMapping.upsert(typeAddress, llvmType); | |
| 1422 | 37090 | return llvmType; | |
| 1423 | } | ||
| 1424 | |||
| 1425 | /** | ||
| 1426 | * Check if this source file is the given runtime module itself | ||
| 1427 | * | ||
| 1428 | * @param runtimeModule Runtime module | ||
| 1429 | * @return Is the runtime module or not | ||
| 1430 | */ | ||
| 1431 | 45267 | public f<bool> SourceFile.isRT(RuntimeModule runtimeModule) { | |
| 1432 | 45267 | const String topLevelName = String(getIdentifyingTopLevelName(runtimeModule)); | |
| 1433 | 45267 | if !this.exportedNameRegistry.contains(topLevelName) { | |
| 1434 | 5849 | return false; | |
| 1435 | } | ||
| 1436 | 39418 | const NameRegistryEntry& entry = this.exportedNameRegistry.get(topLevelName); | |
| 1437 | 78836 | return entry.targetEntry.scope == this.globalScope; | |
| 1438 | 45267 | } | |
| 1439 | |||
| 1440 | // Check if this source file is the string / memory / RTTI / result runtime module itself | ||
| 1441 | 4728 | public inline f<bool> SourceFile.isStringRT() { return this.isRT(RuntimeModule::STRING_RT); } | |
| 1442 | 3951 | public inline f<bool> SourceFile.isMemoryRT() { return this.isRT(RuntimeModule::MEMORY_RT); } | |
| 1443 | 2489 | public inline f<bool> SourceFile.isRttiRT() { return this.isRT(RuntimeModule::RTTI_RT); } | |
| 1444 | 5 | public inline f<bool> SourceFile.isResultRT() { return this.isRT(RuntimeModule::RESULT_RT); } | |
| 1445 | |||
| 1446 | 6253 | f<bool> SourceFile.haveAllDependantsBeenTypeChecked() { | |
| 1447 | 13693 | foreach const SourceFile* dependant : this.dependants { | |
| 1448 | 9686 | if dependant.totalTypeCheckerRuns != 0s { | |
| 1449 | 7430 | continue; | |
| 1450 | } | ||
| 1451 | // Ignore dependants that are part of the same import cycle (i.e. this file transitively depends on them). They | ||
| 1452 | // cannot be type-checked before us either, so waiting on them would deadlock the whole cycle. Searching the import | ||
| 1453 | // graph is expensive, so this is only checked for dependants that were not type-checked yet. | ||
| 1454 | 2256 | if !this.dependsOn(dependant) { | |
| 1455 | 2246 | return false; | |
| 1456 | } | ||
| 1457 | 10 | } | |
| 1458 | 4007 | return true; | |
| 1459 | } | ||
| 1460 | |||
| 1461 | 6024 | public p SourceFile.mergeNameRegistries(const SourceFile& importedSourceFile, const String& importName) { // Public for the RuntimeModuleManager | |
| 1462 | // Retrieve import entry | ||
| 1463 | 6024 | SymbolTableEntry* importEntry = this.globalScope.lookupStrict(importName); | |
| 1464 | 6024 | assert importEntry != nil<SymbolTableEntry*> || importName.startsWith("__"); // Runtime imports start with two underscores | |
| 1465 | |||
| 1466 | 199833 | foreach const Pair<const String&, NameRegistryEntry&> item : importedSourceFile.exportedNameRegistry { | |
| 1467 | 193809 | const String& originalName = item.getFirst(); | |
| 1468 | 193809 | const NameRegistryEntry& entry = item.getSecond(); | |
| 1469 | // Skip if we introduce a transitive dependency | ||
| 1470 | 193809 | if entry.targetScope.sourceFile.globalScope != importedSourceFile.globalScope { | |
| 1471 | 92244 | continue; | |
| 1472 | } | ||
| 1473 | // Add the fully qualified name | ||
| 1474 | 101565 | String newName = importName; | |
| 1475 | 101565 | newName += SCOPE_ACCESS_TOKEN; | |
| 1476 | 101565 | newName += originalName; | |
| 1477 | 101565 | if !this.exportedNameRegistry.contains(newName) { | |
| 1478 | 101565 | this.exportedNameRegistry.upsert(newName, NameRegistryEntry{entry.typeId, entry.targetEntry, entry.targetScope, importEntry}); | |
| 1479 | 101565 | this.trackNameRegistryTargetEntry(entry.targetEntry); | |
| 1480 | 101565 | } | |
| 1481 | // Add the shortened name, considering the name collision. A symbol defined in the importing file itself always | ||
| 1482 | // shadows imported symbols of the same name. Since this merge runs after the file built its own registry (so that | ||
| 1483 | // circular imports work), we must explicitly avoid letting an import overwrite or erase such an own symbol. | ||
| 1484 | 101565 | const NameRegistryEntry* existing = this.exportedNameRegistry.contains(originalName) ? &this.exportedNameRegistry.get(originalName) : nil<NameRegistryEntry*>; | |
| 1485 | 101565 | const bool existingIsOwn = existing != nil<NameRegistryEntry*> && existing.targetScope.sourceFile.globalScope == this.globalScope; | |
| 1486 | 101565 | if !existingIsOwn { | |
| 1487 | 99284 | const bool keepOnCollision = importedSourceFile.alwaysKeepSymbolsOnNameCollision; | |
| 1488 | 99284 | this.addNameRegistryEntry(originalName, entry.typeId, entry.targetEntry, entry.targetScope, keepOnCollision, importEntry); | |
| 1489 | 99284 | } | |
| 1490 | 101565 | } | |
| 1491 | 6024 | } | |
| 1492 | |||
| 1493 | /** | ||
| 1494 | * Recursively merge the exported name registries of all (transitive) dependencies into the respective importing source | ||
| 1495 | * files. Each file merges its direct dependencies' registries exactly once (guarded by registriesMerged), so this is | ||
| 1496 | * safe to call on overlapping subgraphs and on graphs that contain cycles (circular imports). | ||
| 1497 | * | ||
| 1498 | * Must only be called once every reachable file has built its own exported name registry (i.e. after the front-end). | ||
| 1499 | */ | ||
| 1500 | 5001 | public p SourceFile.mergeNameRegistriesRecursive() { | |
| 1501 | 5001 | if this.registriesMerged { | |
| 1502 | 1084 | return; | |
| 1503 | } | ||
| 1504 | 3917 | this.registriesMerged = true; | |
| 1505 | |||
| 1506 | // Merge the direct dependencies' registries into this file. Their own registries are fully built by now, so the | ||
| 1507 | // order in which the reachable files are visited does not matter (even across import cycles). | ||
| 1508 | 6419 | foreach dyn dependency : this.dependencies { | |
| 1509 | 2502 | const String& importName = dependency.getFirst(); | |
| 1510 | 2502 | const SourceFile* sourceFile = dependency.getSecond(); | |
| 1511 | 2502 | this.mergeNameRegistries(*sourceFile, importName); | |
| 1512 | 2502 | } | |
| 1513 | |||
| 1514 | // Recurse into the dependencies to cover the rest of the reachable graph | ||
| 1515 | 6419 | foreach dyn dependency : this.dependencies { | |
| 1516 | 2502 | SourceFile* sourceFile = dependency.getSecond(); | |
| 1517 | 2502 | sourceFile.mergeNameRegistriesRecursive(); | |
| 1518 | 2502 | } | |
| 1519 | 3917 | } | |
| 1520 | |||
| 1521 | /** | ||
| 1522 | * Check whether this source file transitively depends on (imports) the given other source file. | ||
| 1523 | * Used to detect strongly connected components (import cycles) in the dependency graph. | ||
| 1524 | * | ||
| 1525 | * @param other Potential (transitive) dependency | ||
| 1526 | * @return true if this file reaches the other file by following dependency edges | ||
| 1527 | */ | ||
| 1528 | 2256 | f<bool> SourceFile.dependsOn(const SourceFile* other) { | |
| 1529 | // Spice cannot hash pointers, so the visited files are identified by their file path, which is unique per source file | ||
| 1530 | 2256 | UnorderedSet<String> visited; | |
| 1531 | 2256 | Vector<const SourceFile*> worklist; | |
| 1532 | 2256 | worklist.pushBack(this); | |
| 1533 | 2256 | visited.insert(String(this.filePath.toString())); | |
| 1534 | 7158 | while !worklist.isEmpty() { | |
| 1535 | 4912 | const SourceFile* current = worklist.back(); | |
| 1536 | 4912 | worklist.popBack(); | |
| 1537 | 8233 | foreach dyn dep : current.dependencies { | |
| 1538 | 3331 | const SourceFile* dependency = dep.getSecond(); | |
| 1539 | 3331 | if dependency == other { | |
| 1540 | 10 | return true; | |
| 1541 | } | ||
| 1542 | 3321 | const String dependencyPath = String(dependency.filePath.toString()); | |
| 1543 | 3321 | if !visited.contains(dependencyPath) { | |
| 1544 | 2657 | visited.insert(dependencyPath); | |
| 1545 | 2657 | worklist.pushBack(dependency); | |
| 1546 | 2657 | } | |
| 1547 | 3321 | } | |
| 1548 | 4902 | } | |
| 1549 | 2246 | return false; | |
| 1550 | 2256 | } | |
| 1551 | |||
| 1552 | 760 | p SourceFile.dumpCacheStats() { | |
| 1553 | 760 | StringStream cacheStats; | |
| 1554 | 760 | cacheStats << dumpFunctionLookupCacheStatistics() << "\n"; | |
| 1555 | 760 | cacheStats << dumpStructLookupCacheStatistics() << "\n"; | |
| 1556 | 760 | cacheStats << dumpInterfaceLookupCacheStatistics() << "\n"; | |
| 1557 | 760 | cacheStats << dumpUnionLookupCacheStatistics() << "\n"; | |
| 1558 | 760 | this.compilerOutput.cacheStats = cacheStats.str(); | |
| 1559 | 760 | } | |
| 1560 | |||
| 1561 | ✗ | p SourceFile.dumpCompilationStats() { | |
| 1562 | ✗ | const unsigned long sourceFileCount = this.resourceManager.sourceFiles.getSize(); | |
| 1563 | ✗ | const unsigned long totalLineCount = this.resourceManager.getTotalLineCount(); | |
| 1564 | ✗ | const unsigned long totalTypeCount = this.resourceManager.typeRegistry.getTypeCount(); | |
| 1565 | ✗ | const unsigned long allocatedBytes = this.resourceManager.astNodeAlloc.getTotalAllocatedSize(); | |
| 1566 | ✗ | const unsigned long allocationCount = this.resourceManager.astNodeAlloc.getAllocationCount(); | |
| 1567 | ✗ | const unsigned long frontEndDuration = this.resourceManager.frontEndTimer.getDurationInMillis(); | |
| 1568 | ✗ | const unsigned long middleEndDuration = this.resourceManager.middleEndTimer.getDurationInMillis(); | |
| 1569 | ✗ | const unsigned long backEndDuration = this.resourceManager.backEndTimer.getDurationInMillis(); | |
| 1570 | ✗ | const unsigned long totalDuration = frontEndDuration + middleEndDuration + backEndDuration; | |
| 1571 | ✗ | print("\nSuccessfully compiled " + toString(sourceFileCount) + " source file(s)"); | |
| 1572 | ✗ | print(" or " + toString(totalLineCount) + " lines in total.\n"); | |
| 1573 | ✗ | print("Total number of blocks allocated via BlockAllocator: " + formatStorageSize(allocatedBytes)); | |
| 1574 | ✗ | print(" in " + toString(allocationCount) + " allocations.\n"); | |
| 1575 | ✗ | print("Total number of types: " + toString(totalTypeCount) + "\n"); | |
| 1576 | ✗ | print("Total compile time: " + toString(totalDuration) + " ms (frontend: " + toString(frontEndDuration)); | |
| 1577 | ✗ | print(" ms, middle end: " + toString(middleEndDuration) + " ms, backend: " + toString(backEndDuration) + " ms)\n"); | |
| 1578 | ✗ | } | |
| 1579 | |||
| 1580 | 1170 | p SourceFile.dumpOutput(const String& content, const String& caption, const String& fileSuffix) { | |
| 1581 | 1170 | if this.cliOptions.dump.dumpToFiles { | |
| 1582 | // Dump to file | ||
| 1583 | 1170 | const String dumpFileName = this.filePath.getBaseName() + "-" + fileSuffix; | |
| 1584 | 1170 | FilePath dumpFilePath = this.cliOptions.outputDir / dumpFileName; | |
| 1585 | 1170 | dumpFilePath.makeNative(); | |
| 1586 | 1170 | const Result<bool> writeResult = writeFile(dumpFilePath.toString(), content.getRaw()); | |
| 1587 | 1170 | if !writeResult.isOk() { | |
| 1588 | ✗ | const String messageSuffix = "Failed to open file: " + dumpFilePath.toString(); | |
| 1589 | ✗ | const CompilerError error = CompilerError(CompilerErrorType::IO_ERROR, messageSuffix.getRaw()); | |
| 1590 | ✗ | abortWithError(error.getMessage()); | |
| 1591 | ✗ | } | |
| 1592 | 1170 | } else { | |
| 1593 | // Dump to console | ||
| 1594 | ✗ | print("\n" + caption + ":\n" + content); | |
| 1595 | ✗ | } | |
| 1596 | |||
| 1597 | // If the abort after dump is requested, set the abort compilation flag | ||
| 1598 | 1170 | if this.cliOptions.dump.abortAfterDump { | |
| 1599 | // If this is an IR dump whilst having optimization enabled, we may not abort when dumping unoptimized IR, | ||
| 1600 | // because we also have to dump the optimized IR | ||
| 1601 | ✗ | if this.cliOptions.dump.dumpIR && fileSuffix == "ir-code.ll" { | |
| 1602 | ✗ | this.resourceManager.abortCompilation = this.cliOptions.optLevel == OptLevel::O0; | |
| 1603 | ✗ | } else { | |
| 1604 | ✗ | this.resourceManager.abortCompilation = true; | |
| 1605 | ✗ | } | |
| 1606 | ✗ | } | |
| 1607 | 1170 | } | |
| 1608 | |||
| 1609 | 4692 | p SourceFile.visualizerPreamble(StringStream& output) { | |
| 1610 | 4692 | if this.isMainFile { | |
| 1611 | 1795 | output << "digraph {\n rankdir=\"TB\";\n"; | |
| 1612 | 1795 | } else { | |
| 1613 | 2897 | output << "subgraph {\n"; | |
| 1614 | 2897 | } | |
| 1615 | 4692 | output << " label=\"" << this.filePath.toGenericString() << "\";\n"; | |
| 1616 | 4692 | } | |
| 1617 | |||
| 1618 | 9 | p SourceFile.visualizerOutput(const string outputNameRaw, const String& output) { | |
| 1619 | 9 | if this.cliOptions.dump.dumpToFiles { | |
| 1620 | // The dump file names are lower case | ||
| 1621 | ✗ | const String outputName = toLower(String(outputNameRaw)); | |
| 1622 | |||
| 1623 | // Check if graphviz is installed | ||
| 1624 | ✗ | if !isGraphvizInstalled() { | |
| 1625 | ✗ | const CompilerError error = CompilerError(CompilerErrorType::IO_ERROR, "Please check if you have installed Graphviz and added it to the PATH variable"); | |
| 1626 | ✗ | abortWithError(error.getMessage()); | |
| 1627 | ✗ | } | |
| 1628 | |||
| 1629 | // Write to a dot file | ||
| 1630 | ✗ | this.dumpOutput(output, outputName, outputName + ".dot"); | |
| 1631 | |||
| 1632 | // Generate SVG. This only works if the dot code was dumped into a file | ||
| 1633 | ✗ | print("\nGenerating SVG file ... "); | |
| 1634 | ✗ | const String dotFileName = this.filePath.getBaseName() + "-" + outputName + ".dot"; | |
| 1635 | ✗ | FilePath dotFilePath = this.cliOptions.outputDir / dotFileName; | |
| 1636 | ✗ | FilePath svgFilePath = dotFilePath; | |
| 1637 | ✗ | svgFilePath.replaceExtension("svg"); | |
| 1638 | ✗ | dotFilePath.makeNative(); | |
| 1639 | ✗ | svgFilePath.makeNative(); | |
| 1640 | ✗ | Vector<String> dotArgs; | |
| 1641 | ✗ | dotArgs.pushBack(String("-T")); | |
| 1642 | ✗ | dotArgs.pushBack(String("svg")); | |
| 1643 | ✗ | dotArgs.pushBack(String("-o")); | |
| 1644 | ✗ | dotArgs.pushBack(String(svgFilePath.toString())); | |
| 1645 | ✗ | dotArgs.pushBack(String(dotFilePath.toString())); | |
| 1646 | ✗ | const ExecResult _ = exec(String("dot"), dotArgs); | |
| 1647 | ✗ | print("done.\nSVG file can be found at: \"" + svgFilePath.toString() + "\"\n"); | |
| 1648 | ✗ | } else { | |
| 1649 | // Dump to console | ||
| 1650 | 9 | print("\nSerialized " + String(outputNameRaw) + ":\n\n" + output + "\n"); | |
| 1651 | 9 | } | |
| 1652 | |||
| 1653 | // If the abort after dump is requested, set the abort compilation flag | ||
| 1654 | 9 | if this.cliOptions.dump.abortAfterDump { | |
| 1655 | ✗ | this.resourceManager.abortCompilation = true; | |
| 1656 | ✗ | } | |
| 1657 | 9 | } | |
| 1658 | |||
| 1659 | 34586 | p SourceFile.printStatusMessage(const string stage, const CompileStageIOType& in, const CompileStageIOType& out, unsigned long stageRuntime, unsigned short stageRuns = 0s) { | |
| 1660 | 34586 | if this.cliOptions.printDebugOutput { | |
| 1661 | ✗ | const string[5] compilerStageIoTypeName = ["Code", "CST", "AST", "IR", "Obj"]; | |
| 1662 | // Build output string | ||
| 1663 | ✗ | StringStream outputStr; | |
| 1664 | ✗ | outputStr << "[" << stage << "] for " << this.fileName << ": "; | |
| 1665 | ✗ | outputStr << compilerStageIoTypeName[cast<int>(in)] << " --> " << compilerStageIoTypeName[cast<int>(out)]; | |
| 1666 | ✗ | outputStr << " (" << toString(stageRuntime) << " ms"; | |
| 1667 | ✗ | if stageRuns > 0s { | |
| 1668 | ✗ | outputStr << "; " << toString(stageRuns) << " run(s)"; | |
| 1669 | ✗ | } | |
| 1670 | ✗ | outputStr << ")\n"; | |
| 1671 | |||
| 1672 | ✗ | print(outputStr.str()); | |
| 1673 | ✗ | } | |
| 1674 | 34586 | } | |
| 1675 | |||
| 1676 | // ─── tests ──────────────────────────────────────────────────────────────────── | ||
| 1677 | |||
| 1678 | 1 | #[test] | |
| 1679 | f<bool> testGetObjectFilePath() { | ||
| 1680 | 1 | const FilePath testDir = makeUniqueTestDir("spice-object-path-test"); | |
| 1681 | 1 | const FilePath mainPath = testDir / "main.spice"; | |
| 1682 | 1 | const FilePath utilsPath = testDir / "utils.spice"; | |
| 1683 | 1 | writeTestFile(mainPath, ""); | |
| 1684 | 1 | writeTestFile(utilsPath, ""); | |
| 1685 | |||
| 1686 | 1 | string objectFileExt; | |
| 1687 | 1 | FilePath execMainObjectPath; | |
| 1688 | 1 | FilePath execUtilsObjectPath; | |
| 1689 | 1 | FilePath objMainObjectPath; | |
| 1690 | 1 | FilePath objUtilsObjectPath; | |
| 1691 | { | ||
| 1692 | // Source files look up an LLVM target, so use the native one | ||
| 1693 | 1 | CliOptions cliOptions; | |
| 1694 | 1 | cliOptions.targetTriple = llvm::normalizeTargetTriple(llvm::getDefaultTargetTriple()); | |
| 1695 | 1 | cliOptions.isNativeTarget = true; | |
| 1696 | 1 | cliOptions.outputDir = testDir; | |
| 1697 | 1 | cliOptions.outputPath = testDir / "custom-name.o"; | |
| 1698 | 1 | objectFileExt = getOutputFileExtension(cliOptions, OutputContainer::OBJECT_FILE); | |
| 1699 | 1 | GlobalResourceManager resourceManager = GlobalResourceManager(cliOptions); | |
| 1700 | 1 | SourceFile* mainFile = resourceManager.createSourceFile(nil<SourceFile*>, String("main"), mainPath, false); | |
| 1701 | 1 | SourceFile* utils = resourceManager.createSourceFile(mainFile, String("utils"), utilsPath, false); | |
| 1702 | 1 | utils.isMainFile = false; | |
| 1703 | |||
| 1704 | // When linking, every source file emits its object file into the output dir | ||
| 1705 | 1 | cliOptions.outputContainer = OutputContainer::EXECUTABLE; | |
| 1706 | 1 | execMainObjectPath = mainFile.getObjectFilePath(); | |
| 1707 | 1 | execUtilsObjectPath = utils.getObjectFilePath(); | |
| 1708 | // With the object file output container, the object of the main source file is the output file | ||
| 1709 | 1 | cliOptions.outputContainer = OutputContainer::OBJECT_FILE; | |
| 1710 | 1 | objMainObjectPath = mainFile.getObjectFilePath(); | |
| 1711 | 1 | objUtilsObjectPath = utils.getObjectFilePath(); | |
| 1712 | 1 | } | |
| 1713 | |||
| 1714 | // Clean up before asserting, so that a failure does not leave the files behind | ||
| 1715 | 1 | removeTestDir(testDir); | |
| 1716 | 1 | FilePath expectedMainObjectPath = testDir / "main.spice"; | |
| 1717 | 1 | expectedMainObjectPath.replaceExtension(objectFileExt); | |
| 1718 | 1 | FilePath expectedUtilsObjectPath = testDir / "utils.spice"; | |
| 1719 | 1 | expectedUtilsObjectPath.replaceExtension(objectFileExt); | |
| 1720 | 1 | assert execMainObjectPath == expectedMainObjectPath; | |
| 1721 | 1 | assert execUtilsObjectPath == expectedUtilsObjectPath; | |
| 1722 | 1 | assert objMainObjectPath == testDir / "custom-name.o"; | |
| 1723 | 1 | assert objUtilsObjectPath == expectedUtilsObjectPath; | |
| 1724 | 1 | return true; | |
| 1725 | 1 | } | |
| 1726 |