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