symboltablebuilder/symbol-table.spice
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Copyright (c) 2021-2026 ChilliBits. All rights reserved. | ||
| 2 | |||
| 3 | // Std imports | ||
| 4 | import "std/data/map"; | ||
| 5 | import "std/data/vector"; | ||
| 6 | import "std/data/pair"; | ||
| 7 | import "std/type/type-conversion"; | ||
| 8 | import "std/type/types"; | ||
| 9 | import "std/text/json/json-value"; | ||
| 10 | |||
| 11 | // Own imports | ||
| 12 | import "@/ast/ast-nodes"; | ||
| 13 | import "@/symboltablebuilder/qual-type"; | ||
| 14 | import "@/symboltablebuilder/super-type"; | ||
| 15 | import "@/symboltablebuilder/scope"; | ||
| 16 | import "@/symboltablebuilder/symbol-table-entry"; | ||
| 17 | import "@/symboltablebuilder/capture"; | ||
| 18 | import "@/symboltablebuilder/lifecycle"; | ||
| 19 | import "@/util/compiler-warning"; | ||
| 20 | import "@/util/common-util"; | ||
| 21 | import "@/source-file"; | ||
| 22 | |||
| 23 | // Type aliases | ||
| 24 | public type CaptureMap alias Map</*name=*/String, /*capture*/Capture>; // Ordered, since the order determines the capture struct layout | ||
| 25 | public type SymbolMap alias Map</*name=*/String, /*entry*/SymbolTableEntry>; // Ordered, since the order shows in the warnings | ||
| 26 | |||
| 27 | /** | ||
| 28 | * Class for storing information about symbols of the program. | ||
| 29 | * Symbol tables are arranged in a tree structure, so that you can navigate with the .parent property and getChild() method up | ||
| 30 | * and down the tree. | ||
| 31 | */ | ||
| 32 | 262054 | public type SymbolTable struct { | |
| 33 | SymbolTable* parent | ||
| 34 | Scope* scope | ||
| 35 | SymbolMap symbols | ||
| 36 | public CaptureMap captures | ||
| 37 | 271440 | bool capturingRequired = false | |
| 38 | } | ||
| 39 | |||
| 40 | 271440 | public p SymbolTable.ctor(SymbolTable* parent, Scope* scope) { | |
| 41 | 271440 | this.parent = parent; | |
| 42 | 271440 | this.scope = scope; | |
| 43 | 271440 | } | |
| 44 | |||
| 45 | /** | ||
| 46 | * Copy the symbols and captures of another symbol table into this one. The parent and scope stay untouched. This is used | ||
| 47 | * when copying a scope. | ||
| 48 | * | ||
| 49 | * @param other Symbol table to copy from | ||
| 50 | */ | ||
| 51 | 110335 | public p SymbolTable.copyContentsFrom(const SymbolTable& other) { | |
| 52 | 110335 | this.symbols = other.symbols; | |
| 53 | 110335 | this.captures = other.captures; | |
| 54 | 110335 | this.capturingRequired = other.capturingRequired; | |
| 55 | 110335 | } | |
| 56 | |||
| 57 | /** | ||
| 58 | * Count the number of field symbols in this symbol table (struct scopes): | ||
| 59 | * skip anonymous symbols, imports and nested function/struct/union definitions. | ||
| 60 | * | ||
| 61 | * @return Number of field symbols | ||
| 62 | */ | ||
| 63 | 270340 | public f<unsigned long> SymbolTable.getFieldCount() { | |
| 64 | 270340 | result = 0ul; | |
| 65 | 5839865 | foreach Pair<const String&, SymbolTableEntry&> symbolPair : this.symbols { | |
| 66 | 5569525 | SymbolTableEntry& symbol = symbolPair.getSecond(); | |
| 67 | 5569525 | if symbol.anonymous { continue; } | |
| 68 | 5569525 | const QualType& symbolType = symbol.getQualType(); | |
| 69 | 5569525 | if symbolType.is(SuperType::TY_IMPORT) { continue; } | |
| 70 | 5569525 | const ASTNode* declNode = symbol.declNode; | |
| 71 | 5569525 | if declNode.isFctOrProcDef() || declNode.isStructDef() || declNode.isUnionDef() { continue; } | |
| 72 | 915055 | result++; | |
| 73 | 915055 | } | |
| 74 | 270340 | } | |
| 75 | |||
| 76 | /** | ||
| 77 | * Insert a new symbol into the current symbol table. If it is a parameter, append its name to the paramNames vector | ||
| 78 | * | ||
| 79 | * @param name Name of the symbol | ||
| 80 | * @param declNode AST node where the symbol is declared | ||
| 81 | * @return Inserted entry | ||
| 82 | */ | ||
| 83 | 228897 | public f<SymbolTableEntry*> SymbolTable.insert(const String& name, const ASTNode* declNode, bool isAnonymousSymbol = false) { | |
| 84 | 228897 | const bool isGlobal = this.parent == nil<SymbolTable*>; | |
| 85 | // Anonymous symbols do not get an order index (ULONG_MAX_VALUE) | ||
| 86 | 228897 | unsigned long orderIndex = ULONG_MAX_VALUE; | |
| 87 | 228897 | if !isAnonymousSymbol { | |
| 88 | 220093 | orderIndex = 0ul; | |
| 89 | 1757570 | foreach Pair<const String&, SymbolTableEntry&> symbol : this.symbols { | |
| 90 | 1537477 | const SymbolTableEntry& existingEntry = symbol.getSecond(); | |
| 91 | 1537477 | if !existingEntry.anonymous { | |
| 92 | 1537477 | orderIndex++; | |
| 93 | 1537477 | } | |
| 94 | 1537477 | } | |
| 95 | 220093 | } | |
| 96 | // Insert into symbols map. The type is invalid, because concrete types are determined by the type checker later on | ||
| 97 | 228897 | assert !this.symbols.contains(name); | |
| 98 | 228897 | const QualType qualType = QualType(SuperType::TY_INVALID); | |
| 99 | 228897 | ASTNode* mutableDeclNode = cast<ASTNode*>(declNode); | |
| 100 | 228897 | this.symbols.upsert(name, SymbolTableEntry(name, qualType, this.scope, mutableDeclNode, orderIndex, isGlobal)); | |
| 101 | // Set entry to declared | ||
| 102 | 228897 | SymbolTableEntry* entry = &this.symbols.get(name); | |
| 103 | 228897 | entry.updateState(LifecycleState::DECLARED); | |
| 104 | |||
| 105 | // Check if shadowed | ||
| 106 | 228897 | if this.parent != nil<SymbolTable*> && this.parent.lookup(name) != nil<SymbolTableEntry*> && !mutableDeclNode.isParam() && name != RETURN_VARIABLE_NAME { | |
| 107 | 6 | const String warningMsg = "Variable '" + name + "' shadows a variable in a parent scope"; | |
| 108 | 6 | const CompilerWarning warning = CompilerWarning(declNode.codeLoc, CompilerWarningType::SHADOWED_VARIABLE, warningMsg.getRaw()); | |
| 109 | 6 | this.scope.sourceFile.compilerOutput.warnings.pushBack(warning); | |
| 110 | 6 | } | |
| 111 | |||
| 112 | 457794 | return entry; | |
| 113 | 228897 | } | |
| 114 | |||
| 115 | /** | ||
| 116 | * Insert a new anonymous symbol into the current symbol table. | ||
| 117 | * The anonymous symbol will be identified via the definition code location | ||
| 118 | * | ||
| 119 | * @param qualType Type of the symbol | ||
| 120 | * @param declNode AST node where the anonymous symbol is declared | ||
| 121 | * @return Inserted entry | ||
| 122 | */ | ||
| 123 | 8840 | #[ignoreUnusedReturnValue] | |
| 124 | 8840 | public f<SymbolTableEntry*> SymbolTable.insertAnonymous(const QualType& qualType, ASTNode* declNode, unsigned long numericSuffix = 0l) { | |
| 125 | // Check if the anonymous entry already exists | ||
| 126 | 8840 | SymbolTableEntry* existingSymbol = this.lookupAnonymous(declNode, numericSuffix); | |
| 127 | 8840 | if existingSymbol != nil<SymbolTableEntry*> { | |
| 128 | 36 | return existingSymbol; | |
| 129 | } | ||
| 130 | // Otherwise, create an anonymous entry | ||
| 131 | 8804 | const String name = this.getAnonymousSymbolName(declNode, numericSuffix); | |
| 132 | 8804 | SymbolTableEntry* anonSymbol = this.insert(name, declNode, true); | |
| 133 | 8804 | anonSymbol.updateType(qualType, false); | |
| 134 | 8804 | anonSymbol.updateState(LifecycleState::DECLARED); | |
| 135 | 8804 | anonSymbol.updateState(LifecycleState::INITIALIZED); | |
| 136 | 8804 | anonSymbol.anonymous = true; | |
| 137 | 8804 | anonSymbol.used = true; | |
| 138 | 17608 | return anonSymbol; | |
| 139 | 8804 | } | |
| 140 | |||
| 141 | /** | ||
| 142 | * Build the name of an anonymous symbol. It is unique per declaration node and numeric suffix. The node id is used instead | ||
| 143 | * of the node address, so that the name and with that the order of anonymous symbols do not change between compiler runs | ||
| 144 | * | ||
| 145 | * @param declNode AST node where the anonymous symbol is declared | ||
| 146 | * @param numericSuffix Custom numeric suffix | ||
| 147 | * @return Name of the anonymous symbol | ||
| 148 | */ | ||
| 149 | 35250 | f<String> SymbolTable.getAnonymousSymbolName(const ASTNode* declNode, unsigned long numericSuffix) { | |
| 150 | 35250 | SourceFile* sourceFile = this.scope.sourceFile; | |
| 151 | 35250 | const unsigned long nodeId = sourceFile.resourceManager.getNodeId(declNode); | |
| 152 | 35250 | String name = String("anon.") + declNode.codeLoc.toString() + "." + toString(cast<long>(nodeId)); | |
| 153 | 35250 | if numericSuffix > 0l { | |
| 154 | 147 | name += "."; | |
| 155 | 147 | name += toString(cast<long>(numericSuffix)); | |
| 156 | 147 | } | |
| 157 | 70500 | return name; | |
| 158 | 35250 | } | |
| 159 | |||
| 160 | /** | ||
| 161 | * Copy a symbol by its name | ||
| 162 | * | ||
| 163 | * @param originalName Original symbol name | ||
| 164 | * @param newName New symbol name | ||
| 165 | * @return Copied entry | ||
| 166 | */ | ||
| 167 | 16162 | public f<SymbolTableEntry*> SymbolTable.copySymbol(const String& originalName, const String& newName) { | |
| 168 | 16162 | SymbolTableEntry* entryToCopy = this.lookupStrict(originalName); | |
| 169 | 16162 | assert entryToCopy != nil<SymbolTableEntry*>; | |
| 170 | 16162 | assert !this.symbols.contains(newName); | |
| 171 | 16162 | this.symbols.upsert(newName, *entryToCopy); | |
| 172 | 16162 | return &this.symbols.get(newName); | |
| 173 | } | ||
| 174 | |||
| 175 | /** | ||
| 176 | * Check if a symbol exists in the current or any parent scope and return it if possible | ||
| 177 | * | ||
| 178 | * @param name Name of the desired symbol | ||
| 179 | * @return Desired symbol / nullptr if the symbol was not found | ||
| 180 | */ | ||
| 181 | 1318905 | public f<SymbolTableEntry*> SymbolTable.lookup(const String& name) { | |
| 182 | // Check if the symbol exists in the current scope. If yes, take it | ||
| 183 | 1318905 | SymbolTableEntry* entry = this.lookupStrict(name); | |
| 184 | 1318905 | if entry == nil<SymbolTableEntry*> { // Symbol was not found in the current scope | |
| 185 | // We reached the root scope, the symbol does not exist at all | ||
| 186 | 969960 | if this.parent == nil<SymbolTable*> { | |
| 187 | 316461 | return nil<SymbolTableEntry*>; | |
| 188 | } | ||
| 189 | // If we search for the result variable, we want to stop the search when exiting a lambda body | ||
| 190 | 653499 | if name == RETURN_VARIABLE_NAME && this.scope.scopeType == ScopeType::LAMBDA_BODY { | |
| 191 | ✗ | return nil<SymbolTableEntry*>; | |
| 192 | } | ||
| 193 | // If there is a parent scope, continue the search there | ||
| 194 | 653499 | entry = this.parent.lookup(name); | |
| 195 | // Symbol was also not found in all the parent scopes, return nullptr | ||
| 196 | 653499 | if entry == nil<SymbolTableEntry*> { | |
| 197 | 264074 | return nil<SymbolTableEntry*>; | |
| 198 | } | ||
| 199 | |||
| 200 | // Check if this scope requires capturing and capture the variable if appropriate | ||
| 201 | 389425 | const QualType entryType = entry.getQualType(); | |
| 202 | 389425 | const dyn captureSkip = [SuperType::TY_IMPORT, SuperType::TY_FUNCTION, SuperType::TY_PROCEDURE]; | |
| 203 | 389425 | if this.capturingRequired && !this.captures.contains(name) && !entryType.isOneOf(captureSkip, len(captureSkip)) { | |
| 204 | // We need to make the symbol volatile if we are in an async scope and try to access a symbol that is not in an async scope | ||
| 205 | 94 | entry.isVolatile = this.scope.isInAsyncScope() && !entry.scope.isInAsyncScope(); | |
| 206 | // Add the capture to the current scope | ||
| 207 | 94 | this.captures.upsert(name, Capture(entry)); | |
| 208 | 94 | } | |
| 209 | 389425 | } | |
| 210 | 738370 | return entry; | |
| 211 | } | ||
| 212 | |||
| 213 | /** | ||
| 214 | * Check if a symbol exists in the current scope and return it if possible | ||
| 215 | * | ||
| 216 | * @param name Name of the desired symbol | ||
| 217 | * @return Desired symbol / nullptr if the symbol was not found | ||
| 218 | */ | ||
| 219 | 2226492 | public f<SymbolTableEntry*> SymbolTable.lookupStrict(const String& name) { | |
| 220 | 2226492 | if name.isEmpty() { | |
| 221 | ✗ | return nil<SymbolTableEntry*>; | |
| 222 | } | ||
| 223 | // Check if a symbol with this name exists in this scope | ||
| 224 | 2226492 | if this.symbols.contains(name) { | |
| 225 | 1129125 | return &this.symbols.get(name); | |
| 226 | } | ||
| 227 | // Check if a capture with this name exists in this scope | ||
| 228 | 1097367 | if this.captures.contains(name) { | |
| 229 | 55 | Capture& capture = this.captures.get(name); | |
| 230 | 55 | return capture.capturedSymbol; | |
| 231 | } | ||
| 232 | // Otherwise, return a nullptr | ||
| 233 | 1097312 | return nil<SymbolTableEntry*>; | |
| 234 | } | ||
| 235 | |||
| 236 | /** | ||
| 237 | * Check if an order index exists in the current or any parent scope and returns it if possible. | ||
| 238 | * Warning: Unlike the `lookup` method, this one doesn't consider the parent scopes | ||
| 239 | * | ||
| 240 | * @param orderIndex Order index of the desired symbol | ||
| 241 | * @return Desired symbol / nullptr if the symbol was not found | ||
| 242 | */ | ||
| 243 | 224611 | public f<SymbolTableEntry*> SymbolTable.lookupStrictByIndex(const unsigned int orderIndex) { | |
| 244 | 2325956 | foreach Pair<const String&, SymbolTableEntry&> symbolPair : this.symbols { | |
| 245 | 2325956 | SymbolTableEntry& symbol = symbolPair.getSecond(); | |
| 246 | 2325956 | if symbol.orderIndex == cast<unsigned long>(orderIndex) { | |
| 247 | 224611 | return &symbol; | |
| 248 | } | ||
| 249 | 2101345 | } | |
| 250 | ✗ | return nil<SymbolTableEntry*>; | |
| 251 | } | ||
| 252 | |||
| 253 | /** | ||
| 254 | * Check if an anonymous symbol exists in the current scope and return it if possible | ||
| 255 | * | ||
| 256 | * @param codeLoc Definition code loc | ||
| 257 | * @param numericSuffix Numeric suffix of the anonymous symbol | ||
| 258 | * @return Anonymous symbol | ||
| 259 | */ | ||
| 260 | 26446 | public f<SymbolTableEntry*> SymbolTable.lookupAnonymous(const ASTNode* declNode, unsigned long numericSuffix = 0l) { | |
| 261 | 26446 | const String name = this.getAnonymousSymbolName(declNode, numericSuffix); | |
| 262 | 52892 | return this.lookup(name); | |
| 263 | 26446 | } | |
| 264 | |||
| 265 | /** | ||
| 266 | * Check if a symbol exists in the current or any parent scope and return it if possible. | ||
| 267 | * If the target symbol is an alias symbol, resolve the alias container entry. | ||
| 268 | * | ||
| 269 | * @param name Name of the desired symbol | ||
| 270 | * @return Desired symbol / nil if the symbol was not found + alias or not | ||
| 271 | */ | ||
| 272 | 71216 | public f<Pair<SymbolTableEntry*, bool>> SymbolTable.lookupWithAliasResolution(const String& name) { | |
| 273 | 71216 | SymbolTableEntry* entry = this.lookup(name); | |
| 274 | 71216 | if entry == nil<SymbolTableEntry*> { | |
| 275 | 54581 | return Pair<SymbolTableEntry*, bool>(entry, false); | |
| 276 | } | ||
| 277 | 16635 | const QualType entryType = entry.getQualType(); | |
| 278 | 16635 | if !entryType.is(SuperType::TY_ALIAS) { | |
| 279 | 16629 | return Pair<SymbolTableEntry*, bool>(entry, false); | |
| 280 | } | ||
| 281 | |||
| 282 | // We have an alias type here, resolve it | ||
| 283 | 6 | assert entry.scope.isRootScope(); | |
| 284 | 6 | entry.used = true; | |
| 285 | 6 | const String aliasedContainerEntryName = entry.name + ALIAS_CONTAINER_SUFFIX; | |
| 286 | 6 | SymbolTableEntry* aliasedTypeContainerEntry = entry.scope.lookupStrict(aliasedContainerEntryName); | |
| 287 | 6 | assert aliasedTypeContainerEntry != nil<SymbolTableEntry*>; | |
| 288 | 12 | return Pair<SymbolTableEntry*, bool>(aliasedTypeContainerEntry, true); | |
| 289 | 6 | } | |
| 290 | |||
| 291 | /** | ||
| 292 | * Check if a capture exists in the current or any parent scope scope and return it if possible | ||
| 293 | * | ||
| 294 | * @param name Name of the desired captured symbol | ||
| 295 | * @return Capture / nullptr if the capture was not found | ||
| 296 | */ | ||
| 297 | 1158115 | public f<Capture*> SymbolTable.lookupCapture(const String& name) { | |
| 298 | // Check if the capture exists in the current scope. If yes, take it | ||
| 299 | 1158115 | Capture* capture = this.lookupCaptureStrict(name); | |
| 300 | 1158115 | if capture != nil<Capture*> { | |
| 301 | 128 | return capture; | |
| 302 | } | ||
| 303 | |||
| 304 | // We reached the root scope, the symbol does not exist at all | ||
| 305 | 1157987 | if this.parent == nil<SymbolTable*> { | |
| 306 | 283646 | return nil<Capture*>; | |
| 307 | } | ||
| 308 | |||
| 309 | 874341 | return this.parent.lookupCapture(name); | |
| 310 | } | ||
| 311 | |||
| 312 | /** | ||
| 313 | * Check if a capture exists in the current scope and return it if possible | ||
| 314 | * | ||
| 315 | * @param name Name of the desired captured symbol | ||
| 316 | * @return Capture / nullptr if the capture was not found | ||
| 317 | */ | ||
| 318 | 1158115 | public f<Capture*> SymbolTable.lookupCaptureStrict(const String& captureName) { | |
| 319 | // If available in the current scope, return it | ||
| 320 | 1158115 | if this.captures.contains(captureName) { | |
| 321 | 128 | return &this.captures.get(captureName); | |
| 322 | } | ||
| 323 | // Otherwise, return nullptr | ||
| 324 | 1157987 | return nil<Capture*>; | |
| 325 | } | ||
| 326 | |||
| 327 | /** | ||
| 328 | * Set capturing for this scope to required. | ||
| 329 | */ | ||
| 330 | 144 | public p SymbolTable.setCapturingRequired() { | |
| 331 | 144 | this.capturingRequired = true; | |
| 332 | 144 | } | |
| 333 | |||
| 334 | /** | ||
| 335 | * Deletes an existing anonymous symbol | ||
| 336 | * | ||
| 337 | * @param name Anonymous symbol name | ||
| 338 | */ | ||
| 339 | 11662 | public p SymbolTable.deleteAnonymous(const String& name) { | |
| 340 | 11662 | if this.symbols.contains(name) { | |
| 341 | 6554 | this.symbols.remove(name); | |
| 342 | 6554 | } | |
| 343 | 11662 | } | |
| 344 | |||
| 345 | /** | ||
| 346 | * Stringify a symbol table to a human-readable form. This is used to realize dumps of symbol tables | ||
| 347 | * | ||
| 348 | * Example: | ||
| 349 | * { | ||
| 350 | * "name": "<SymbolTableName>" | ||
| 351 | * "symbols": [ | ||
| 352 | * ... (SymbolTableEntry) | ||
| 353 | * ], | ||
| 354 | * "children": [ | ||
| 355 | * ... (SymbolTable) | ||
| 356 | * ] | ||
| 357 | * } | ||
| 358 | * | ||
| 359 | * The returned heap-allocated JSON value is owned by the caller. | ||
| 360 | * | ||
| 361 | * @return Symbol table if form of a string | ||
| 362 | */ | ||
| 363 | 277588 | public f<JsonValue*> SymbolTable.toJSON() { | |
| 364 | // Collect all symbols, sorted by name | ||
| 365 | 277588 | Map<String, SymbolTableEntry*> sortedSymbols; | |
| 366 | 653464 | foreach Pair<const String&, SymbolTableEntry&> symbol : this.symbols { | |
| 367 | 375876 | sortedSymbols.upsert(symbol.getFirst(), &symbol.getSecond()); | |
| 368 | 375876 | } | |
| 369 | 277588 | JsonValue* jsonSymbols = __new<JsonValue>(JsonValueKind::JSON_ARRAY); | |
| 370 | 653464 | foreach Pair<const String&, SymbolTableEntry*&> symbol : sortedSymbols { | |
| 371 | 375876 | SymbolTableEntry* entry = symbol.getSecond(); | |
| 372 | 375876 | jsonSymbols.addArrayItem(entry.toJSON()); | |
| 373 | 375876 | } | |
| 374 | |||
| 375 | // Collect all captures, sorted by name | ||
| 376 | 277588 | Map<String, Capture*> sortedCaptures; | |
| 377 | 277680 | foreach Pair<const String&, Capture&> capture : this.captures { | |
| 378 | 92 | sortedCaptures.upsert(capture.getFirst(), &capture.getSecond()); | |
| 379 | 92 | } | |
| 380 | 277588 | JsonValue* jsonCaptures = __new<JsonValue>(JsonValueKind::JSON_ARRAY); | |
| 381 | 277680 | foreach Pair<const String&, Capture*&> capture : sortedCaptures { | |
| 382 | 92 | Capture* capturePtr = capture.getSecond(); | |
| 383 | 92 | jsonCaptures.addArrayItem(capturePtr.toJSON()); | |
| 384 | 92 | } | |
| 385 | |||
| 386 | // Generate json | ||
| 387 | 277588 | result = __new<JsonValue>(JsonValueKind::JSON_OBJECT); | |
| 388 | 277588 | setJsonField(result, String("symbols"), jsonSymbols); | |
| 389 | 277588 | setJsonField(result, String("captures"), jsonCaptures); | |
| 390 | 277588 | } | |
| 391 |