All files / transpiler/logic/symbols SymbolTable.ts

99.3% Statements 142/143
97.1% Branches 67/69
100% Functions 41/41
99.26% Lines 135/136

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531                                            1137x     1137x       1137x             1137x           1137x             487x 487x 18x   469x       487x 487x 234x   253x               164x 376x               7x 7x             912x             2x             3x 3x 2x     1x               71x   71x 177x   13x 13x 6x       71x             255x             6x 6x 13x 13x 6x       6x             975x 975x 788x   975x                               224x 224x 125x 125x     224x                               12x 12x                         111x 111x               24x                 255x               26x               4x 2x                   27x                 12x               26x               4x 3x                       510x               168x                     9x   7x 7x 6x 6x       7x 13x                     53x         64x 64x             142x 142x 142x 142x 142x             71x 71x 166x   71x               29x   14x   1x             13x   27x     10x         8x 8x       2x     13x   3x       10x 21x   10x   3x 1x   1x           9x 18x   9x 18x   9x 18x     9x   2x   4x     2x                 7x 10x 5x                   2x   1x     1x                             131x 131x 373x         6x 6x 2x           131x 129x       2x 6x         2x 2x   2x 2x 1x       1x 1x 1x 1x             2x   1x                
/**
 * Unified Symbol Table
 * Stores symbols from all source languages and detects conflicts
 */
 
import ISymbol from "../../../utils/types/ISymbol";
import ESourceLanguage from "../../../utils/types/ESourceLanguage";
import ESymbolKind from "../../../utils/types/ESymbolKind";
import LiteralUtils from "../../../utils/LiteralUtils";
import IConflict from "./types/IConflict";
import IStructFieldInfo from "./types/IStructFieldInfo";
 
/**
 * Central symbol table for cross-language interoperability
 *
 * Per user requirement: Symbol conflicts between C-Next and C/C++ are ERRORS.
 * - ERROR: Same symbol defined in C-Next and C/C++
 * - OK: Multiple `extern` declarations in C (declaration, not definition)
 * - OK: Function overloads in C++ (different signatures)
 */
class SymbolTable {
  /** All symbols indexed by name */
  private readonly symbols: Map<string, ISymbol[]> = new Map();
 
  /** Symbols indexed by source file */
  private readonly byFile: Map<string, ISymbol[]> = new Map();
 
  /** Struct field information: struct name -> (field name -> field info) */
  private readonly structFields: Map<string, Map<string, IStructFieldInfo>> =
    new Map();
 
  /**
   * Issue #196 Bug 3: Track C struct names that need the 'struct' keyword
   * These are structs defined as 'struct Name { ... }' without typedef
   * In C, they must be referred to as 'struct Name', not just 'Name'
   */
  private readonly needsStructKeyword: Set<string> = new Set();
 
  /**
   * Issue #208: Track enum backing type bit widths
   * C++14 typed enums: enum Name : uint8_t { ... } have explicit bit widths
   */
  private readonly enumBitWidth: Map<string, number> = new Map();
 
  /**
   * Add a symbol to the table
   */
  addSymbol(symbol: ISymbol): void {
    // Add to name index
    const existing = this.symbols.get(symbol.name);
    if (existing) {
      existing.push(symbol);
    } else {
      this.symbols.set(symbol.name, [symbol]);
    }
 
    // Add to file index
    const fileSymbols = this.byFile.get(symbol.sourceFile);
    if (fileSymbols) {
      fileSymbols.push(symbol);
    } else {
      this.byFile.set(symbol.sourceFile, [symbol]);
    }
  }
 
  /**
   * Add multiple symbols at once
   */
  addSymbols(symbols: ISymbol[]): void {
    for (const symbol of symbols) {
      this.addSymbol(symbol);
    }
  }
 
  /**
   * Get a symbol by name (returns first match, or undefined)
   */
  getSymbol(name: string): ISymbol | undefined {
    const symbols = this.symbols.get(name);
    return symbols?.[0];
  }
 
  /**
   * Get all symbols with a given name (for overload detection)
   */
  getOverloads(name: string): ISymbol[] {
    return this.symbols.get(name) ?? [];
  }
 
  /**
   * Check if a symbol exists
   */
  hasSymbol(name: string): boolean {
    return this.symbols.has(name);
  }
 
  /**
   * Check if a symbol has conflicts
   */
  hasConflict(name: string): boolean {
    const symbols = this.symbols.get(name);
    if (!symbols || symbols.length <= 1) {
      return false;
    }
 
    return this.detectConflict(symbols) !== null;
  }
 
  /**
   * Get all conflicts in the symbol table
   * Per user requirement: Strict errors for cross-language conflicts
   */
  getConflicts(): IConflict[] {
    const conflicts: IConflict[] = [];
 
    for (const [, symbols] of this.symbols) {
      if (symbols.length <= 1) continue;
 
      const conflict = this.detectConflict(symbols);
      if (conflict) {
        conflicts.push(conflict);
      }
    }
 
    return conflicts;
  }
 
  /**
   * Get symbols by source file
   */
  getSymbolsByFile(file: string): ISymbol[] {
    return this.byFile.get(file) ?? [];
  }
 
  /**
   * Get symbols by source language
   */
  getSymbolsByLanguage(lang: ESourceLanguage): ISymbol[] {
    const result: ISymbol[] = [];
    for (const symbols of this.symbols.values()) {
      for (const symbol of symbols) {
        if (symbol.sourceLanguage === lang) {
          result.push(symbol);
        }
      }
    }
    return result;
  }
 
  /**
   * Get all symbols
   */
  getAllSymbols(): ISymbol[] {
    const result: ISymbol[] = [];
    for (const symbols of this.symbols.values()) {
      result.push(...symbols);
    }
    return result;
  }
 
  /**
   * Add struct field information
   * @param structName Name of the struct
   * @param fieldName Name of the field
   * @param fieldType Type of the field (e.g., "uint32_t")
   * @param arrayDimensions Optional array dimensions if field is an array
   */
  addStructField(
    structName: string,
    fieldName: string,
    fieldType: string,
    arrayDimensions?: number[],
  ): void {
    let fields = this.structFields.get(structName);
    if (!fields) {
      fields = new Map();
      this.structFields.set(structName, fields);
    }
 
    fields.set(fieldName, {
      type: fieldType,
      arrayDimensions,
    });
  }
 
  /**
   * Get struct field type
   * @param structName Name of the struct
   * @param fieldName Name of the field
   * @returns Field type or undefined if not found
   */
  getStructFieldType(
    structName: string,
    fieldName: string,
  ): string | undefined {
    const fields = this.structFields.get(structName);
    return fields?.get(fieldName)?.type;
  }
 
  /**
   * Get struct field info (type and array dimensions)
   * @param structName Name of the struct
   * @param fieldName Name of the field
   * @returns Field info or undefined if not found
   */
  getStructFieldInfo(
    structName: string,
    fieldName: string,
  ): IStructFieldInfo | undefined {
    const fields = this.structFields.get(structName);
    return fields?.get(fieldName);
  }
 
  /**
   * Issue #196 Bug 3: Mark a struct as requiring 'struct' keyword in C
   * @param structName Name of the struct (e.g., "NamedPoint")
   */
  markNeedsStructKeyword(structName: string): void {
    this.needsStructKeyword.add(structName);
  }
 
  /**
   * Issue #196 Bug 3: Check if a struct requires 'struct' keyword in C
   * @param structName Name of the struct
   * @returns true if the struct was defined as 'struct Name { ... }' without typedef
   */
  checkNeedsStructKeyword(structName: string): boolean {
    return this.needsStructKeyword.has(structName);
  }
 
  /**
   * Issue #196 Bug 3: Get all struct names requiring 'struct' keyword
   * @returns Array of struct names
   */
  getAllNeedsStructKeyword(): string[] {
    return Array.from(this.needsStructKeyword);
  }
 
  /**
   * Issue #196 Bug 3: Restore needsStructKeyword from cache
   * @param structNames Array of struct names requiring 'struct' keyword
   */
  restoreNeedsStructKeyword(structNames: string[]): void {
    for (const name of structNames) {
      this.needsStructKeyword.add(name);
    }
  }
 
  /**
   * Issue #208: Add enum bit width for a typed enum
   * @param enumName Name of the enum (e.g., "EPressureType")
   * @param bitWidth Bit width from backing type (e.g., 8 for uint8_t)
   */
  addEnumBitWidth(enumName: string, bitWidth: number): void {
    this.enumBitWidth.set(enumName, bitWidth);
  }
 
  /**
   * Issue #208: Get enum bit width for a typed enum
   * @param enumName Name of the enum
   * @returns Bit width or undefined if not a typed enum
   */
  getEnumBitWidth(enumName: string): number | undefined {
    return this.enumBitWidth.get(enumName);
  }
 
  /**
   * Issue #208: Get all enum bit widths for cache serialization
   * @returns Map of enum name -> bit width
   */
  getAllEnumBitWidths(): Map<string, number> {
    return this.enumBitWidth;
  }
 
  /**
   * Issue #208: Restore enum bit widths from cache
   * @param bitWidths Map of enum name -> bit width
   */
  restoreEnumBitWidths(bitWidths: Map<string, number>): void {
    for (const [enumName, width] of bitWidths) {
      this.enumBitWidth.set(enumName, width);
    }
  }
 
  /**
   * Get all fields for a struct
   * @param structName Name of the struct
   * @returns Map of field names to field info, or undefined if struct not found
   */
  getStructFields(
    structName: string,
  ): Map<string, IStructFieldInfo> | undefined {
    return this.structFields.get(structName);
  }
 
  /**
   * Get all struct fields for cache serialization
   * @returns Map of struct name -> (field name -> field info)
   */
  getAllStructFields(): Map<string, Map<string, IStructFieldInfo>> {
    return this.structFields;
  }
 
  /**
   * Restore struct fields from cache
   * Merges cached fields into the existing structFields map
   * @param fields Map of struct name -> (field name -> field info)
   */
  restoreStructFields(
    fields: Map<string, Map<string, IStructFieldInfo>>,
  ): void {
    for (const [structName, fieldMap] of fields) {
      // Get or create the struct's field map
      let existingFields = this.structFields.get(structName);
      if (!existingFields) {
        existingFields = new Map();
        this.structFields.set(structName, existingFields);
      }
 
      // Merge fields
      for (const [fieldName, fieldInfo] of fieldMap) {
        existingFields.set(fieldName, fieldInfo);
      }
    }
  }
 
  /**
   * Get struct names defined in a specific source file
   * @param file Source file path
   * @returns Array of struct names defined in that file
   */
  getStructNamesByFile(file: string): string[] {
    const fileSymbols = this.byFile.get(file) ?? [];
    // Issue #196: Include any symbol that has struct fields registered
    // The dual-parse strategy may register typedef'd anonymous structs as variables
    // (e.g., "typedef struct { ... } Rectangle;" -> Rectangle has kind=variable)
    // But the C parser still adds struct fields for it
    const symbolNames = fileSymbols.map((s) => s.name);
    return symbolNames.filter((name) => this.structFields.has(name));
  }
 
  /**
   * Clear all symbols
   */
  clear(): void {
    this.symbols.clear();
    this.byFile.clear();
    this.structFields.clear();
    this.needsStructKeyword.clear();
    this.enumBitWidth.clear();
  }
 
  /**
   * Get symbol count
   */
  get size(): number {
    let count = 0;
    for (const symbols of this.symbols.values()) {
      count += symbols.length;
    }
    return count;
  }
 
  /**
   * Detect if a set of symbols with the same name represents a conflict
   */
  private detectConflict(symbols: ISymbol[]): IConflict | null {
    // Filter out pure declarations (extern in C) - they don't count as definitions
    const definitions = symbols.filter((s) => !s.isDeclaration);
 
    if (definitions.length <= 1) {
      // 0 or 1 definitions = no conflict
      return null;
    }
 
    // Issue #221: Filter out function parameters from conflict detection
    // Function parameters have a parent (their containing function) but their name
    // is NOT qualified with the parent prefix (unlike scope-level variables like Math_counter).
    // Parameters with the same name in different functions are not conflicts.
    const globalDefinitions = definitions.filter((def) => {
      // If no parent, it's a global symbol - keep it
      if (!def.parent) return true;
 
      // Variables with a parent need special handling
      if (def.kind === ESymbolKind.Variable) {
        // Scope-level variables have qualified names (e.g., "Math_counter" with parent "Math")
        // Function parameters have unqualified names (e.g., "x" with parent "Math_add")
        // If the name starts with parent_, it's a scope-level variable - keep it
        // If not, it's a function parameter - filter it out
        const isQualifiedName = def.name.startsWith(def.parent + "_");
        return isQualifiedName;
      }
 
      // Non-variable symbols with parents (functions, enums, etc.) are kept
      return true;
    });
 
    if (globalDefinitions.length <= 1) {
      // After filtering parameters, 0 or 1 definitions = no conflict
      return null;
    }
 
    // Check for C++ function overloads (different signatures are OK)
    const cppFunctions = definitions.filter(
      (s) => s.sourceLanguage === ESourceLanguage.Cpp && s.signature,
    );
    if (cppFunctions.length === definitions.length) {
      // All are C++ functions with signatures
      const uniqueSignatures = new Set(cppFunctions.map((s) => s.signature));
      Eif (uniqueSignatures.size === cppFunctions.length) {
        // All signatures are unique = valid overload, no conflict
        return null;
      }
    }
 
    // Check for cross-language conflict (C-Next vs C or C++)
    // Issue #221: Use globalDefinitions for C-Next to exclude function parameters
    const cnextDefs = globalDefinitions.filter(
      (s) => s.sourceLanguage === ESourceLanguage.CNext,
    );
    const cDefs = globalDefinitions.filter(
      (s) => s.sourceLanguage === ESourceLanguage.C,
    );
    const cppDefs = globalDefinitions.filter(
      (s) => s.sourceLanguage === ESourceLanguage.Cpp,
    );
 
    if (cnextDefs.length > 0 && (cDefs.length > 0 || cppDefs.length > 0)) {
      // C-Next + C/C++ conflict = ERROR
      const locations = globalDefinitions.map(
        (s) =>
          `${s.sourceLanguage.toUpperCase()} (${s.sourceFile}:${s.sourceLine})`,
      );
 
      return {
        symbolName: globalDefinitions[0].name,
        definitions: globalDefinitions,
        severity: "error",
        message: `Symbol conflict: '${globalDefinitions[0].name}' is defined in multiple languages:\n  ${locations.join("\n  ")}\nRename the C-Next symbol to resolve.`,
      };
    }
 
    // Multiple definitions in same language (excluding overloads) = ERROR
    if (cnextDefs.length > 1) {
      const locations = cnextDefs.map((s) => `${s.sourceFile}:${s.sourceLine}`);
      return {
        symbolName: cnextDefs[0].name,
        definitions: cnextDefs,
        severity: "error",
        message: `Symbol conflict: '${cnextDefs[0].name}' is defined multiple times in C-Next:\n  ${locations.join("\n  ")}`,
      };
    }
 
    // Same symbol in C and C++ - typically OK (same symbol)
    // But if they have different types, might be a warning
    if (cDefs.length > 0 && cppDefs.length > 0) {
      // For now, allow C/C++ to share symbols (common pattern)
      return null;
    }
 
    return null;
  }
 
  /**
   * Issue #461: Resolve external const array dimensions
   *
   * After all symbols are collected, scan for variable symbols with unresolved
   * array dimensions (stored as strings instead of numbers). For each unresolved
   * dimension, look up the const value in the symbol table and resolve it.
   *
   * This handles the case where array dimensions reference constants from
   * external .cnx files that were not available during initial symbol collection.
   */
  resolveExternalArrayDimensions(): void {
    // Build a map of all const values from the symbol table
    const constValues = new Map<string, number>();
    for (const symbol of this.getAllSymbols()) {
      if (
        symbol.kind === ESymbolKind.Variable &&
        symbol.isConst &&
        symbol.initialValue !== undefined
      ) {
        const value = LiteralUtils.parseIntegerLiteral(symbol.initialValue);
        if (value !== undefined) {
          constValues.set(symbol.name, value);
        }
      }
    }
 
    // If no const values found, nothing to resolve
    if (constValues.size === 0) {
      return;
    }
 
    // Scan all variable symbols for unresolved array dimensions
    for (const symbol of this.getAllSymbols()) {
      if (
        symbol.kind === ESymbolKind.Variable &&
        symbol.isArray &&
        symbol.arrayDimensions
      ) {
        let modified = false;
        const resolvedDimensions = symbol.arrayDimensions.map((dim) => {
          // If dimension is numeric, keep it
          const numericValue = Number.parseInt(dim, 10);
          if (!Number.isNaN(numericValue)) {
            return dim;
          }
 
          // Try to resolve from const values
          const constValue = constValues.get(dim);
          Eif (constValue !== undefined) {
            modified = true;
            return String(constValue);
          }
 
          // Keep original (unresolved macro reference)
          return dim;
        });
 
        if (modified) {
          // Update the symbol's array dimensions
          symbol.arrayDimensions = resolvedDimensions;
        }
      }
    }
  }
}
 
export default SymbolTable;