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 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 | 97x 1993x 774x 774x 774x 774x 774x 774x 774x 774x 774x 774x 774x 774x 3526x 3526x 603x 2923x 1042x 1042x 1042x 1042x 1042x 82x 82x 139x 139x 475x 956x 2388x 2388x 1969x 447x 1702x 1702x 2488x 1702x 1x 2x 1x 1x 1x 1x 302x 302x 683x 302x 146x 146x 146x 7x 7x 8x 24x 46x 8x 8x 14x 4560x 4560x 3341x 170x 299x 299x 44x 299x 54x 54x 15x 18x 418x 418x 3341x 170x 299x 299x 28x 299x 299x 13x 1969x 1572x 1372x 170x 302x 302x 302x 42x 302x 302x 302x 26x 302x 12x 12x 12x 289x 289x 683x 289x 289x 289x 751x 751x 9x 9x 2x 289x 42x 12x 30x 3x 27x 21x 42x 21x 20x 1x 2x 2x 2x 1x 5x 5x 5x 4x 4x 14x 21x 42x 21x 21x 21x 21x 1x 20x 40x 20x 40x 20x 40x 20x 25x 25x 20x 2x 2x 4x 2x 18x 18x 5x 13x 1x 12x 298x 298x 1x 297x 297x 297x 510x 503x 7x 297x 297x 297x 297x 771x 252x 519x 519x 519x 1x 518x 518x 518x 518x 8x 510x 297x 771x 75x 696x 149x 547x 7x 7x 15x 7x 18x 7x 11x 11x 16x 10x 6x 1x 5x 5x 5x 5x 6x 11x 11x 22x 22x 22x 22x 6x 16x 11x 248x 248x 155x 155x 248x 150x 150x 328x 328x 1299x 81x 81x 57x 57x 108x 57x 338x 12x 7x 7x 7x 7x 7x 12x 62x 74x 74x 27x 491x 31x 8x 160x 83x 16x 16x 16x 16x 16x 96x 96x 96x 48x 11x 48x 48x 10x 30x 30x 12x 12x 404x 33x 27x 27x 21x 304x 30x 30x 30x 6x 26x 26x 6x 1x 1x 6x 9x 1x 28x 28x 1827x 1815x 12x 12x 1x 11x 2x 16x 5x 31x 8x 1x 288x 288x 278x 10x 1672x 76x 76x 8x 8x 1387x 1387x 1669x 1669x 57x 1387x 10x 124x 2x 2x 2x 2x 1x 1x 1x 1x 1x 2x 1x 1219x 1219x 1219x 1219x 1219x 1219x 1219x 1219x 1219x 1219x 1219x | /**
* Unified Symbol Table
* Stores symbols from all source languages and detects conflicts
*
* ADR-055 Phase 7: Fully typed symbol storage using discriminated unions.
* - TSymbol: C-Next symbols (rich type system with TType)
* - TCSymbol: C header symbols (string types)
* - TCppSymbol: C++ header symbols (string types)
*/
import DeclarationSite from "../../../utils/DeclarationSite";
import ScopeUtils from "../../../utils/ScopeUtils";
import { produce, enableMapSet } from "immer";
import ESourceLanguage from "../../../utils/types/ESourceLanguage";
import LiteralUtils from "../../../utils/LiteralUtils";
import IConflict from "../../types/IConflict";
import IStructFieldInfo from "../../types/symbols/IStructFieldInfo";
import IStructSymbolState from "../../types/symbols/IStructSymbolState";
import TJsonSafe from "../../../utils/types/TJsonSafe";
import TSymbol from "../../types/symbols/TSymbol";
import TCSymbol from "../../types/symbols/c/TCSymbol";
import TCppSymbol from "../../types/symbols/cpp/TCppSymbol";
import TAnySymbol from "../../types/symbols/TAnySymbol";
import IStructSymbol from "../../types/symbols/IStructSymbol";
import IEnumSymbol from "../../types/symbols/IEnumSymbol";
import IFunctionSymbol from "../../types/symbols/IFunctionSymbol";
import IVariableSymbol from "../../types/symbols/IVariableSymbol";
import TypeResolver from "../../../utils/TypeResolver";
import type ITargetCapabilities from "../../types/ITargetCapabilities";
// Enable immer support for Map and Set (must be called once at module scope)
enableMapSet();
/** Create a fresh initial struct symbol state */
function createInitialStructState(): IStructSymbolState {
return {
opaqueTypes: new Set(),
typedefStructTypes: new Map(),
structTagAliases: new Map(),
typedefToTag: new Map(),
structTagsWithBodies: new Set(),
pointerTypedefs: new Set(),
};
}
/**
* 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 {
// ========================================================================
// C-Next Symbol Storage (TSymbol)
// ========================================================================
/** All C-Next TSymbols indexed by bare name (ADR-055: `init`, not `Motor__init`) */
private readonly tSymbols: Map<string, TSymbol[]> = new Map();
/**
* All C-Next TSymbols indexed by transpiled C name — their canonical identity.
*
* ADR-063 makes the qualified name injective, so it identifies a symbol
* without needing a scope to interpret it. The bare-name index above answers
* a different question ("what does `init` mean *here*?", which needs ADR-057
* local->scope->global context); this one answers "which symbol is
* `Motor__init`?". Codegen holds the latter, and before this index existed it
* had to ask the former and got a silent empty result.
*
* For a global symbol both indexes share a key, since its bare name already
* is its canonical identity.
*/
private readonly tSymbolsByCName: Map<string, TSymbol[]> = new Map();
/** C-Next TSymbols indexed by source file */
private readonly tSymbolsByFile: Map<string, TSymbol[]> = new Map();
// ========================================================================
// C Symbol Storage (TCSymbol)
// ========================================================================
/** All C symbols indexed by name */
private readonly cSymbols: Map<string, TCSymbol[]> = new Map();
/** C symbols indexed by source file */
private readonly cSymbolsByFile: Map<string, TCSymbol[]> = new Map();
// ========================================================================
// C++ Symbol Storage (TCppSymbol)
// ========================================================================
/** All C++ symbols indexed by name */
private readonly cppSymbols: Map<string, TCppSymbol[]> = new Map();
/** C++ symbols indexed by source file */
private readonly cppSymbolsByFile: Map<string, TCppSymbol[]> = new Map();
// ========================================================================
// Auxiliary Data (shared across languages)
// ========================================================================
/** 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 #985 recovery: names of external function-like MACROS recovered by
* translation-unit preprocessing (ExternalDeclarationOracle) for headers cnext
* could not preprocess standalone. Macros have no declaration to parse, so
* only their names are known; consulted by the undeclared-`global.`-call check
* so it does not false-positive on a real macro (e.g. FreeRTOS pdMS_TO_TICKS).
* Recovered FUNCTIONS are registered as full symbols (with signatures) instead.
*/
private readonly externalDeclarationNames: Set<string> = new Set();
/**
* Issue #958: Immutable struct symbol state — additive only, query-time resolution.
* Replaces separate opaqueTypes, typedefStructTypes, structTagAliases fields.
*/
private structState: IStructSymbolState = createInitialStructState();
/**
* 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();
// ========================================================================
// C-Next Symbol Methods (TSymbol)
// ========================================================================
/**
* Append a symbol to one of the multi-value indexes, creating the bucket on
* first use.
*
* Every index in this class is `Map<string, T[]>` and was maintained by its
* own copy of this get/push-or-set block. Adding an index meant writing the
* block again and remembering `clear()` — the "edit it in two places"
* anti-pattern CLAUDE.md calls the worst in the project.
*/
private static appendToIndex<T>(
index: Map<string, T[]>,
key: string,
symbol: T,
): void {
const existing = index.get(key);
if (existing) {
existing.push(symbol);
} else {
index.set(key, [symbol]);
}
}
/**
* Add a C-Next TSymbol to the table
*/
addTSymbol(symbol: TSymbol): void {
// #1285: read the identity the symbol was built with rather than deriving
// it again. The symbol and its index key can no longer disagree, because
// there is no second derivation to disagree with -- previously this called
// the encoder a second time and stayed correct only because it happened to
// be the same encoder.
const cName = symbol.fullyQualifiedCName;
SymbolTable.appendToIndex(this.tSymbols, symbol.name, symbol);
SymbolTable.appendToIndex(this.tSymbolsByCName, cName, symbol);
SymbolTable.appendToIndex(this.tSymbolsByFile, symbol.sourceFile, symbol);
// Auto-register struct fields for TypeResolver.getMemberTypeInfo()
if (symbol.kind === "struct") {
this.registerStructFields(symbol, cName);
}
}
/**
* Register struct fields in structFields map for cross-file type resolution.
* Called automatically when adding struct symbols.
* Issue #981: Now preserves string dimensions (macro names) for proper array detection.
*
* @param cName The struct's transpiled C name, already computed by the caller —
* passed in rather than re-derived so this symbol's identity has one producer
* per call, matching the canonical-identity rule the index above relies on.
*/
private registerStructFields(struct: IStructSymbol, cName: string): void {
for (const [fieldName, fieldInfo] of struct.fields) {
// Convert TType to string for structFields map
const typeString = TypeResolver.getTypeName(fieldInfo.type);
this.addStructField(
cName,
fieldName,
typeString,
fieldInfo.dimensions && fieldInfo.dimensions.length > 0
? fieldInfo.dimensions
: undefined,
);
}
}
/**
* Add multiple C-Next TSymbols at once
*/
addTSymbols(symbols: readonly TSymbol[]): void {
for (const symbol of symbols) {
this.addTSymbol(symbol);
}
}
/**
* Get a TSymbol by name (returns first match, or undefined)
*/
getTSymbol(name: string): TSymbol | undefined {
const symbols = this.tSymbols.get(name);
return symbols?.[0];
}
/**
* Get all TSymbols with a given name (for overload detection)
*/
getTOverloads(name: string): TSymbol[] {
return this.tSymbols.get(name) ?? [];
}
/**
* Get TSymbols by source file
*/
getTSymbolsByFile(file: string): TSymbol[] {
return this.tSymbolsByFile.get(file) ?? [];
}
/**
* Get all TSymbols
*/
getAllTSymbols(): TSymbol[] {
const result: TSymbol[] = [];
for (const symbols of this.tSymbols.values()) {
result.push(...symbols);
}
return result;
}
/**
* Get all struct symbols (type-safe filtering)
*/
getStructSymbols(): IStructSymbol[] {
return this.getAllTSymbols().filter(
(s): s is IStructSymbol => s.kind === "struct",
);
}
/**
* Get all enum symbols (type-safe filtering)
*/
getEnumSymbols(): IEnumSymbol[] {
return this.getAllTSymbols().filter(
(s): s is IEnumSymbol => s.kind === "enum",
);
}
/**
* Get all function symbols (type-safe filtering)
*/
getFunctionSymbols(): IFunctionSymbol[] {
return this.getAllTSymbols().filter(
(s): s is IFunctionSymbol => s.kind === "function",
);
}
/**
* Get all variable symbols (type-safe filtering)
*/
getVariableSymbols(): IVariableSymbol[] {
return this.getAllTSymbols().filter(
(s): s is IVariableSymbol => s.kind === "variable",
);
}
/**
* Get struct field type directly from TSymbol storage.
* This method queries IStructSymbol.fields directly, eliminating the need
* for the separate structFields Map for C-Next symbols.
*
* @param structName Name of the struct
* @param fieldName Name of the field
* @returns Field type string or undefined if not found
*/
getTStructFieldType(
structName: string,
fieldName: string,
): string | undefined {
const struct = this.getTOverloads(structName).find(
(s): s is IStructSymbol => s.kind === "struct",
);
if (!struct) {
return undefined;
}
const field = struct.fields.get(fieldName);
return field ? TypeResolver.getTypeName(field.type) : undefined;
}
/**
* Check if a TSymbol exists by name
*/
hasTSymbol(name: string): boolean {
return this.tSymbols.has(name);
}
/**
* Get TSymbol count
*/
getTSize(): number {
let count = 0;
for (const symbols of this.tSymbols.values()) {
count += symbols.length;
}
return count;
}
// ========================================================================
// C Symbol Methods (TCSymbol)
// ========================================================================
/**
* Add a C symbol to the table
* Issue #981: Also register struct fields for type resolution
*/
addCSymbol(symbol: TCSymbol): void {
SymbolTable.appendToIndex(this.cSymbols, symbol.name, symbol);
SymbolTable.appendToIndex(this.cSymbolsByFile, symbol.sourceFile, symbol);
// Issue #981: Register struct fields for getMemberTypeInfo() lookups
if (symbol.kind === "struct" && symbol.fields) {
this.registerCStructFields(symbol.name, symbol.fields);
}
}
/**
* Register C struct fields in structFields map for cross-file type resolution.
* Issue #981: Required for macro-sized array field detection on local struct variables.
*/
private registerCStructFields(
structName: string,
fields: ReadonlyMap<
string,
import("../../types/symbols/c/ICFieldInfo").default
>,
): void {
for (const [fieldName, fieldInfo] of fields) {
this.addStructField(
structName,
fieldName,
fieldInfo.type,
fieldInfo.arrayDimensions,
);
}
}
/**
* Add multiple C symbols at once
*/
addCSymbols(symbols: TCSymbol[]): void {
for (const symbol of symbols) {
this.addCSymbol(symbol);
}
}
/**
* Register external declaration names recovered via TU preprocessing.
* @see externalDeclarationNames
*/
addExternalDeclarationNames(names: ReadonlySet<string>): void {
for (const name of names) {
this.externalDeclarationNames.add(name);
}
}
/** Whether a name was recovered as an external function / function-like macro. */
hasExternalDeclaration(name: string): boolean {
return this.externalDeclarationNames.has(name);
}
/**
* Get a C symbol by name (returns first match, or undefined)
*/
getCSymbol(name: string): TCSymbol | undefined {
const symbols = this.cSymbols.get(name);
return symbols?.[0];
}
/**
* Get all C symbols with a given name
*/
getCOverloads(name: string): TCSymbol[] {
return this.cSymbols.get(name) ?? [];
}
/**
* Get C symbols by source file
*/
getCSymbolsByFile(file: string): TCSymbol[] {
return this.cSymbolsByFile.get(file) ?? [];
}
/**
* Get all C symbols
*/
getAllCSymbols(): TCSymbol[] {
const result: TCSymbol[] = [];
for (const symbols of this.cSymbols.values()) {
result.push(...symbols);
}
return result;
}
// ========================================================================
// C++ Symbol Methods (TCppSymbol)
// ========================================================================
/**
* Add a C++ symbol to the table
*/
addCppSymbol(symbol: TCppSymbol): void {
SymbolTable.appendToIndex(this.cppSymbols, symbol.name, symbol);
SymbolTable.appendToIndex(this.cppSymbolsByFile, symbol.sourceFile, symbol);
}
/**
* Add multiple C++ symbols at once
*/
addCppSymbols(symbols: TCppSymbol[]): void {
for (const symbol of symbols) {
this.addCppSymbol(symbol);
}
}
/**
* Get a C++ symbol by name (returns first match, or undefined)
*/
getCppSymbol(name: string): TCppSymbol | undefined {
const symbols = this.cppSymbols.get(name);
return symbols?.[0];
}
/**
* Get all C++ symbols with a given name
*/
getCppOverloads(name: string): TCppSymbol[] {
return this.cppSymbols.get(name) ?? [];
}
/**
* Get C++ symbols by source file
*/
getCppSymbolsByFile(file: string): TCppSymbol[] {
return this.cppSymbolsByFile.get(file) ?? [];
}
/**
* Get all C++ symbols
*/
getAllCppSymbols(): TCppSymbol[] {
const result: TCppSymbol[] = [];
for (const symbols of this.cppSymbols.values()) {
result.push(...symbols);
}
return result;
}
// ========================================================================
// Cross-Language Methods
// ========================================================================
/**
* Get all symbols across all languages
*/
getAllSymbols(): TAnySymbol[] {
return [
...this.getAllTSymbols(),
...this.getAllCSymbols(),
...this.getAllCppSymbols(),
];
}
/**
* Get first symbol matching a name across all languages.
* Searches TSymbol, then C, then C++ collections.
* Used by ISymbolLookup interface for constructor detection.
*/
getSymbol(name: string): TAnySymbol | undefined {
return (
this.getTSymbol(name) ?? this.getCSymbol(name) ?? this.getCppSymbol(name)
);
}
/**
* Get all overloads for a name across all languages.
*
* C-Next symbols are matched on their **bare** name, so a scoped member is
* found by the name it carries inside its scope (`readValue`), not by its
* transpiled C name. Callers holding a transpiled C name want
* getOverloadsByCName instead.
*/
getOverloads(name: string): TAnySymbol[] {
return [
...this.getTOverloads(name),
...this.getCOverloads(name),
...this.getCppOverloads(name),
];
}
/**
* Get all C-Next overloads whose canonical identity is the given transpiled
* C name (e.g. "Motor__init").
*
* The inverse of ScopeUtils.getTranspiledCName: that builds the name, this
* resolves it back to the symbol. Exact match — no decomposition, so nothing
* has to re-derive how a qualified name is spelled.
*/
getTOverloadsByCName(cName: string): TSymbol[] {
return this.tSymbolsByCName.get(cName) ?? [];
}
/**
* Get all overloads across all languages for a transpiled C name.
*
* Use this wherever the caller already holds a generated C identifier —
* codegen and anything downstream of it. C and C++ symbols have no scopes,
* so their name is already their identity and they are matched directly;
* C-Next symbols are matched through the canonical-identity index.
*/
getOverloadsByCName(cName: string): TAnySymbol[] {
return [
...this.getTOverloadsByCName(cName),
...this.getCOverloads(cName),
...this.getCppOverloads(cName),
];
}
/**
* Get symbols by source file across all languages
*/
getSymbolsByFile(file: string): TAnySymbol[] {
return [
...this.getTSymbolsByFile(file),
...this.getCSymbolsByFile(file),
...this.getCppSymbolsByFile(file),
];
}
/**
* Get symbols by source language
*/
getSymbolsByLanguage(lang: ESourceLanguage): TAnySymbol[] {
switch (lang) {
case ESourceLanguage.CNext:
return this.getAllTSymbols();
case ESourceLanguage.C:
return this.getAllCSymbols();
case ESourceLanguage.Cpp:
return this.getAllCppSymbols();
}
}
/**
* Check if a symbol exists in any language
*/
hasSymbol(name: string): boolean {
return (
this.tSymbols.has(name) ||
this.cSymbols.has(name) ||
this.cppSymbols.has(name)
);
}
/**
* Get total symbol count
*/
get size(): number {
return this.getTSize() + this.getCSize() + this.getCppSize();
}
/**
* Get C symbol count
*/
getCSize(): number {
let count = 0;
for (const symbols of this.cSymbols.values()) {
count += symbols.length;
}
return count;
}
/**
* Get C++ symbol count
*/
getCppSize(): number {
let count = 0;
for (const symbols of this.cppSymbols.values()) {
count += symbols.length;
}
return count;
}
// ========================================================================
// Conflict Detection
// ========================================================================
/**
* Check if a symbol has conflicts
*/
hasConflict(name: string): boolean {
const allSymbols = this.getOverloads(name);
Iif (allSymbols.length <= 1) {
return false;
}
return this.detectConflict(allSymbols) !== null;
}
/**
* Get all conflicts in the symbol table
* Per user requirement: Strict errors for cross-language conflicts
*/
getConflicts(): IConflict[] {
const conflicts: IConflict[] = [];
const allNames = new Set<string>();
// Collect all symbol names from all languages
for (const name of this.tSymbols.keys()) allNames.add(name);
for (const name of this.cSymbols.keys()) allNames.add(name);
for (const name of this.cppSymbols.keys()) allNames.add(name);
for (const name of allNames) {
const symbols = this.getOverloads(name);
if (symbols.length <= 1) continue;
const conflict = this.detectConflict(symbols);
if (conflict) {
conflicts.push(conflict);
}
}
return conflicts;
}
/**
* Issue #221: function parameters must not count as conflicting definitions.
* They have a parent, but their name is not qualified with the parent prefix.
*
* Only C/C++ symbols are filtered. A C-Next variable is always kept: at this
* point a scope-level variable and a function parameter are indistinguishable,
* so the original code returned true down both of its branches.
*/
private static isNotFunctionParameter(def: TAnySymbol): boolean {
if (
def.sourceLanguage === ESourceLanguage.CNext &&
def.kind === "variable"
) {
return true;
}
if ("parent" in def && def.parent) {
// A non-variable with a parent is a real definition; a variable with a
// parent may be a function parameter, so it is dropped.
return def.kind !== "variable";
}
return true;
}
/**
* True when every definition is a C++ function and all their signatures
* differ -- overloads, which are legal rather than a conflict.
*
* Currently redundant (#1180): no path in detectConflict reports a conflict
* between two C++ symbols, so an all-C++ group returns null whether this
* short-circuits or falls through. Verified by mutation -- forcing this to
* false left all 49 SymbolTable tests passing. Extracted here unchanged
* rather than deleted, because which way to resolve it (drop the branch, or
* add the same-signature conflict it implies) is a behavior decision.
*/
private static areAllDistinctCppOverloads(
globalDefinitions: TAnySymbol[],
): boolean {
const cppFunctions = globalDefinitions.filter(
(s) =>
s.sourceLanguage === ESourceLanguage.Cpp &&
s.kind === "function" &&
"parameters" in s,
);
if (cppFunctions.length !== globalDefinitions.length) {
return false;
}
const signatures = cppFunctions.map((f) => {
Eif ("parameters" in f && f.parameters) {
const params = f.parameters as ReadonlyArray<{ type?: string }>;
return params.map((p) => p.type ?? "").join(",");
}
return "";
});
return new Set(signatures).size === cppFunctions.length;
}
/**
* The blocks declaring the scope these symbols belong to, or "" at global scope.
*
* #1334: ADR-016 lets a scope be reopened, so the members that collide may sit
* in different blocks of a scope spread across several files. Naming only the
* member definitions leaves the reader to find those blocks themselves.
*/
private static scopeDeclarationNote(symbol: TSymbol): string {
const scope = symbol.scope;
Eif (ScopeUtils.isGlobalScope(scope) || scope.declarationSites.size === 0) {
return "";
}
// Sorted through DeclarationSite, not `.sort()`: these keys end in a line
// number, and a text sort orders `:10` ahead of `:3` (SonarCloud S2871).
const sites = [...scope.declarationSites]
.sort(DeclarationSite.compare)
.map(DeclarationSite.displaySite);
// Indented: the CLI's error format treats an unindented line as a new
// diagnostic, so an unindented header here is dropped by any consumer that
// parses stderr -- including the test harness, which kept the sites and lost
// the sentence introducing them.
return `\n scope '${scope.name}' is declared in:\n ${sites.join("\n ")}`;
}
/**
* Where a definition is, as a symbol carries it.
*
* #1334: the two conflict producers formatted this differently, so the same fact
* printed two ways depending on which path found it. Symbols carry no column
* (IBaseSymbol has sourceFile/sourceLine only), so line is the finest granularity
* available. The rendering itself lives in DeclarationSite -- see there for why it
* is a basename and why the ordering needs a comparator.
*/
/**
* The language a definition came from, as a reader knows it.
*
* Private because SymbolTable is the only consumer today; promote it beside
* ESourceLanguage if a second one appears.
*/
private static languageName(symbol: TAnySymbol): string {
const names: Record<ESourceLanguage, string> = {
[ESourceLanguage.CNext]: "C-Next",
[ESourceLanguage.C]: "C",
[ESourceLanguage.Cpp]: "C++",
};
return names[symbol.sourceLanguage];
}
private static locationOf(symbol: TAnySymbol): string {
return DeclarationSite.display(symbol.sourceFile, symbol.sourceLine);
}
/**
* Detect if a set of symbols with the same name represents a conflict
*/
private detectConflict(symbols: TAnySymbol[]): IConflict | null {
// Filter out pure declarations (extern in C) - they don't count as definitions
const definitions = symbols.filter(
(s) => !("isDeclaration" in s && s.isDeclaration),
);
Iif (definitions.length <= 1) {
// 0 or 1 definitions = no conflict
return null;
}
const globalDefinitions = definitions.filter(
SymbolTable.isNotFunctionParameter,
);
Iif (globalDefinitions.length <= 1) {
return null;
}
if (SymbolTable.areAllDistinctCppOverloads(globalDefinitions)) {
return null;
}
// Check for cross-language conflict (C-Next vs C or C++)
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,
);
// Issue #967: Only global-scope C-Next symbols can conflict with C/C++ symbols.
// Scoped symbols (e.g., Touch.read) live in a namespace and don't compete
// with C's global symbols (e.g., POSIX read()).
const conflictingCnextDefs = cnextDefs.filter((s) => {
const tSymbol = s as TSymbol;
return tSymbol.scope.name === "";
});
if (
conflictingCnextDefs.length > 0 &&
(cDefs.length > 0 || cppDefs.length > 0)
) {
const conflictingDefs = [...conflictingCnextDefs, ...cDefs, ...cppDefs];
// #1334: both conflict kinds render the location the same way, through
// `locationOf` -- these producers used to spell it differently (`LANG
// (file:line)` here, bare `file:line` below), which is one decision written
// twice. Only the language ANNOTATION is branch-specific, and it has to stay:
// the message says the definitions are in multiple languages but not which is
// which, and for a `.h` the reader cannot tell C from C++ -- the very
// distinction detectAssemblySyntax points users at this message for.
//
// Deduplicated because one C declaration can yield two symbols at one position
// (`typedef struct {...} helper;` registers both the tag and the alias), which
// printed the same file:line twice and told the reader nothing.
const locations = [
...new Set(
conflictingDefs.map(
(definition) =>
`${SymbolTable.locationOf(definition)} (${SymbolTable.languageName(definition)})`,
),
),
];
return {
code: "E0425",
symbolName: conflictingDefs[0].name,
definitions: conflictingDefs,
severity: "error",
sourceFile: conflictingDefs[0].sourceFile,
line: conflictingDefs[0].sourceLine,
column: 0,
// The remediation line is INDENTED like the locations. The CLI's error format
// treats an unindented line as the start of a new diagnostic, so an
// unindented sentence here is dropped by any consumer that parses stderr --
// including the test harness, which would capture the locations and silently
// lose this line (scripts/test-utils.ts, continuation-line branch).
message: `Symbol conflict: '${conflictingDefs[0].name}' is defined in multiple languages:\n ${locations.join("\n ")}\n Rename the C-Next symbol to resolve.`,
};
}
// Multiple definitions in same language (excluding overloads) = ERROR
const cnextConflict = this.detectCNextDuplicate(cnextDefs);
if (cnextConflict) {
return cnextConflict;
}
// Same symbol in C and C++ - typically OK (same symbol)
if (cDefs.length > 0 && cppDefs.length > 0) {
return null;
}
return null;
}
// ========================================================================
// MISRA C:2012 Rule 5.1 - External Identifier Length
// ========================================================================
/**
* Reject two external identifiers that are indistinguishable within the
* target's significant-character limit (MISRA C:2012 Rule 5.1, issue #1307).
*
* C99 5.2.4.1 guarantees only 31 significant initial characters in an
* external identifier, and `Scope__member` spends that budget on the
* *encoding* rather than on the author's names -- the `__` separator costs two
* characters per level and the scope name costs its full length. Two members
* of `TemperatureSensorController` agree for 29 characters before their own
* names begin, so a conforming implementation may treat them as one
* identifier. On a hosted GCC nothing happens; on the minimal embedded
* toolchain that is C-Next's target audience the two variables silently
* become one, and `cppcheck --addon=misra` does not report it.
*
* Scoped to identifiers C-Next itself generates, and among those to the ones
* with external linkage:
* - A C or C++ header's identifiers are not this transpiler's to rename, and
* C++ names are mangled rather than truncated.
* - `static` members (ADR-016 emits `private` as `static`) have internal
* linkage, which C99 gives a 63-character budget under a different rule.
* - Types (struct/enum/bitmap/register) have no linkage at all.
* - Two identifiers that are equal outright are `detectConflict`'s job
* (ADR-063, #1117), not this one.
*
* @param targetCapabilities The target's identifier significance limits
* @returns One conflict per group of identifiers sharing a truncated prefix
*/
detectMISRA51Conflicts(targetCapabilities: ITargetCapabilities): IConflict[] {
const limit = targetCapabilities?.significantExternalIdentifierChars;
if (limit === undefined) {
return [];
}
const byPrefix = SymbolTable.groupExternalCNextSymbolsByPrefix(
this.getAllSymbols(),
limit,
);
const conflicts: IConflict[] = [];
for (const group of byPrefix.values()) {
if (group.length < 2) {
continue;
}
conflicts.push(SymbolTable.buildMISRA51Conflict(group, limit));
}
return conflicts;
}
/**
* Bucket every externally-linked C-Next identifier by its truncated prefix.
*
* Deduplicated by `fullyQualifiedCName`: one declaration can be registered
* more than once when a file is reached along two include paths, and identity
* -- not arrival count -- is what a linker sees.
*/
private static groupExternalCNextSymbolsByPrefix(
symbols: TAnySymbol[],
limit: number,
): Map<string, TSymbol[]> {
const byPrefix = new Map<string, TSymbol[]>();
const seen = new Set<string>();
for (const symbol of symbols) {
if (!SymbolTable.hasExternalLinkage(symbol)) {
continue;
}
const cnextSymbol = symbol as TSymbol;
const identifier = cnextSymbol.fullyQualifiedCName;
if (seen.has(identifier)) {
continue;
}
seen.add(identifier);
const prefix = identifier.slice(0, limit);
const group = byPrefix.get(prefix);
if (group) {
group.push(cnextSymbol);
} else {
byPrefix.set(prefix, [cnextSymbol]);
}
}
return byPrefix;
}
/**
* True when the symbol is a C-Next declaration emitted with external linkage.
*
* Only variables and functions have linkage in C; a typedef or an enumeration
* constant names nothing the linker resolves.
*/
private static hasExternalLinkage(symbol: TAnySymbol): boolean {
if (symbol.sourceLanguage !== ESourceLanguage.CNext) {
return false;
}
if (symbol.kind !== "variable" && symbol.kind !== "function") {
return false;
}
return symbol.isExported;
}
/**
* Build the Rule 5.1 diagnostic for one group of colliding identifiers.
*
* Names the C-Next names the author wrote (`cnxScopedName`) rather than the
* generated identifiers, per #1292 -- reporting `Scope__member` names
* something that appears in no source file.
*/
private static buildMISRA51Conflict(
group: TSymbol[],
limit: number,
): IConflict {
const shared = group[0].fullyQualifiedCName.slice(0, limit);
// Rendered through DeclarationSite like every other conflict location. This
// producer landed concurrently with #1342 and carried its own `basename`
// copy, which is ambiguous for the `can/config.cnx` + `uart/config.cnx`
// layout #1133 supports -- both render as `config.cnx`.
const locations = group
.map(
(symbol) =>
` ${symbol.cnxScopedName} (${DeclarationSite.display(symbol.sourceFile, symbol.sourceLine)})`,
)
.join("\n");
return {
code: "E0204",
symbolName: group[0].name,
definitions: [...group],
severity: "error",
sourceFile: group[0].sourceFile,
line: group[0].sourceLine,
column: 0,
message:
`External identifiers are not distinct within the target's ` +
`${limit} significant characters (MISRA C:2012 Rule 5.1). ` +
`All of these generate an identifier beginning '${shared}':\n${locations}\n` +
` Shorten the scope name or the member names so the first ${limit} ` +
`characters differ.`,
};
}
/**
* Two definitions of the same C-Next symbol in the same scope = ERROR.
*
* Issue #817: grouped by scope AND kind — symbols in different scopes do not
* conflict (`Foo.enabled` and `Bar.enabled` generate distinct C names), and
* symbols of different kinds do not either (a variable `LED` and a scope `LED`
* are distinct).
*
* Extracted from detectConflict so that method stays under SonarCloud's
* cognitive-complexity limit; the #1333 scope-reopening branch pushed it over.
*/
private detectCNextDuplicate(cnextDefs: TAnySymbol[]): IConflict | null {
if (cnextDefs.length <= 1) {
return null;
}
const byScopeAndKind = this.groupCNextSymbolsByScopeAndKind(cnextDefs);
for (const symbols of byScopeAndKind.values()) {
if (symbols.length <= 1) {
continue;
}
// #1333: a scope declaration is not a definition in the sense this rule
// means. Declaring `scope Lib` a second time REOPENS it and adds members;
// it does not redefine it -- the model ADR-002:256 described ("one
// namespace can span files") and ADR-016 now carries forward. Without
// this, a scope could not be split across files, and could not even be
// reopened within one file, which defeats the organizational purpose
// scopes exist for.
//
// Members still conflict normally: they are grouped by the scope's own
// identity, so two `Lib.useIt` definitions collide whichever block they
// were written in.
if (symbols[0].kind === "scope") {
continue;
}
const locations = symbols.map(SymbolTable.locationOf);
// #1285: the symbol's own source-language name. This was built here by
// hand from `scope.name`, which is the leaf -- at depth two it reported
// `Inner.tick` for a symbol the author writes as `Outer.Inner.tick`.
const displayName = symbols[0].cnxScopedName;
// #1334: when the members belong to a scope, name where that scope is
// DECLARED as well as where the members are defined. A scope spanning four
// files is where a duplicate member is hardest to find, and the blocks are
// exactly what the reader needs to look through.
//
// This is also what makes declarationSites observable: without a consumer
// it would be a write-only field, testable only by unit tests that reach
// into it -- the shape #1330's review caught as a method with no caller.
const scopeSites = SymbolTable.scopeDeclarationNote(symbols[0]);
return {
code: "E0425",
symbolName: displayName,
definitions: symbols,
severity: "error",
// Report at the first offending definition. Each symbol carries its own
// position, so a member declared in two blocks of a scope spanning four
// files names the block it actually came from (#1334).
sourceFile: symbols[0].sourceFile,
line: symbols[0].sourceLine,
column: 0,
message: `Symbol conflict: '${displayName}' is defined multiple times in C-Next:\n ${locations.join("\n ")}${scopeSites}`,
};
}
return null;
}
/**
* Issue #817: Group C-Next symbols by scope name and kind.
*
* Symbols in different scopes don't conflict (Foo.enabled vs Bar.enabled
* generate Foo_enabled and Bar_enabled). Symbols with different kinds also
* don't conflict (variable LED vs scope LED are distinct).
*
* @param symbols C-Next symbols to group (must all be TSymbol)
* @returns Map from "scopeName:kind" key to array of symbols
*/
private groupCNextSymbolsByScopeAndKind(
symbols: TAnySymbol[],
): Map<string, TSymbol[]> {
const byScopeAndKind = new Map<string, TSymbol[]>();
for (const def of symbols) {
const tSymbol = def as TSymbol;
// #1285: key on the scope's own identity, not its leaf name. Two distinct
// scopes can share a leaf (`Outer.Inner` and `Other.Inner`), and keying on
// the leaf grouped their members together -- reporting a conflict between
// symbols that never shared a scope.
const key = `${tSymbol.scope.fullyQualifiedCName}:${tSymbol.kind}`;
const existing = byScopeAndKind.get(key);
if (existing) {
existing.push(tSymbol);
} else {
byScopeAndKind.set(key, [tSymbol]);
}
}
return byScopeAndKind;
}
// ========================================================================
// Struct Field Information
// ========================================================================
/**
* Add struct field information.
* Issue #981: Accept (number | string)[] for arrayDimensions to support macro-sized arrays.
* @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 - numbers for resolved, strings for macros
*/
addStructField(
structName: string,
fieldName: string,
fieldType: string,
arrayDimensions?: readonly (number | string)[],
): void {
let fields = this.structFields.get(structName);
if (!fields) {
fields = new Map();
this.structFields.set(structName, fields);
}
// Copy to mutable array for storage
fields.set(fieldName, {
type: fieldType,
arrayDimensions: arrayDimensions ? [...arrayDimensions] : undefined,
});
}
/**
* 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);
}
/**
* 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 struct field types as a simple map (fieldName -> typeName).
* Used by code generation for nested struct initializers.
* @param structName Name of the struct
* @returns Map of field names to type strings, or undefined if struct not found
*/
getStructFieldTypes(structName: string): Map<string, string> | undefined {
const fields = this.structFields.get(structName);
if (!fields) return undefined;
const result = new Map<string, string>();
for (const [fieldName, info] of fields) {
result.set(fieldName, info.type);
}
return result;
}
/**
* 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) {
let existingFields = this.structFields.get(structName);
Eif (!existingFields) {
existingFields = new Map();
this.structFields.set(structName, existingFields);
}
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.getSymbolsByFile(file);
const symbolNames = fileSymbols.map((s) => s.name);
return symbolNames.filter((name) => this.structFields.has(name));
}
// ========================================================================
// Struct Keyword Tracking
// ========================================================================
/**
* 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);
}
}
// ========================================================================
// Struct Symbol State (Issue #948, #958) — immer-managed, additive only
// ========================================================================
/**
* Issue #1225: capture the whole struct state for the cache.
*
* The return type is derived from `IStructSymbolState`, so a field added
* there makes this method fail to compile until it is written here. That
* replaces a hand-maintained capture list which silently omitted
* `pointerTypedefs` when #1164 added it — a warm-cache build then emitted a
* header that contradicted the real typedef.
*
* `typedefToTag` is captured even though `restoreStructTagAliases` derives
* it: covering every key removes "is this one derived?" as something anyone
* has to remember.
*/
serializeStructState(): TJsonSafe<Required<IStructSymbolState>> {
return {
opaqueTypes: Array.from(this.structState.opaqueTypes),
typedefStructTypes: Array.from(this.structState.typedefStructTypes),
structTagAliases: Array.from(this.structState.structTagAliases),
typedefToTag: Array.from(this.structState.typedefToTag),
structTagsWithBodies: Array.from(this.structState.structTagsWithBodies),
pointerTypedefs: Array.from(this.structState.pointerTypedefs),
};
}
/**
* Issue #1225: the keys `serializeStructState` produces.
*
* Derived by running the serializer rather than listing them, so it cannot
* fall behind the interface. Two callers need it and must agree: the reader
* that validates an entry's struct state, and the cache-config fingerprint
* that invalidates entries written under an older shape.
*/
static structStateKeys(): string[] {
return Object.keys(new SymbolTable().serializeStructState());
}
/**
* Issue #1225: restore struct state from the cache.
*
* Additive, like every other mutation of this state: a warm build merges
* cached facts into whatever the current run has already learned.
*
* Coverage is enforced twice. The `IStructSymbolState` annotation below
* fails to compile if a field is not revived, and `mergeStructState` walks
* the object rather than naming fields, so it cannot skip one.
*/
restoreStructState(state: TJsonSafe<Required<IStructSymbolState>>): void {
const revived: Required<IStructSymbolState> = {
opaqueTypes: new Set(state.opaqueTypes),
typedefStructTypes: new Map(state.typedefStructTypes),
structTagAliases: new Map(state.structTagAliases),
typedefToTag: new Map(state.typedefToTag),
structTagsWithBodies: new Set(state.structTagsWithBodies),
pointerTypedefs: new Set(state.pointerTypedefs),
};
this.structState = produce(this.structState, (draft) => {
SymbolTable.mergeStructState(draft, revived);
});
}
/**
* Merge every field of `incoming` into `draft`.
*
* Deliberately generic: it reads the keys off the object instead of listing
* them, so a new field is merged without anyone editing this method. The
* throw is the counterpart — a field that is neither a Set nor a Map has no
* merge rule, and failing loudly beats the silent skip that produced #1225.
*/
private static mergeStructState(
draft: IStructSymbolState,
incoming: IStructSymbolState,
): void {
// Safe: `incoming` is built from an IStructSymbolState-annotated literal
// directly above, so its own keys are exactly the interface's keys.
const keys = Object.keys(incoming) as Array<keyof IStructSymbolState>;
for (const key of keys) {
const target = draft[key];
const source = incoming[key];
if (target instanceof Set && source instanceof Set) {
for (const member of source) {
target.add(member);
}
} else if (target instanceof Map && source instanceof Map) {
for (const [entryKey, entryValue] of source) {
target.set(entryKey, entryValue);
}
} else E{
// TypeError, not Error: the condition above is an instanceof check
// (SonarCloud S7786), and what went wrong is genuinely a field whose
// type has no merge rule.
throw new TypeError(
`SymbolTable.mergeStructState: no merge rule for struct-state field "${key}". ` +
`Add one -- a silently skipped field is how #1225 happened.`,
);
}
}
}
/**
* Issue #948: Mark a typedef as aliasing an opaque (forward-declared) struct type.
* @param typeName Typedef name (e.g., "widget_t")
*/
markOpaqueType(typeName: string): void {
this.structState = produce(this.structState, (draft) => {
draft.opaqueTypes.add(typeName);
});
}
/**
* Issue #957/#1164: record a typedef of a pointer to a struct.
*/
markPointerTypedef(typeName: string): void {
this.structState = produce(this.structState, (draft) => {
draft.pointerTypedefs.add(typeName);
});
}
/**
* Issue #957/#1164: is this typedef a pointer to a struct?
*
* Such a type must never be forward-declared as `typedef struct X X;` -- the
* header has to include the one that defines it.
*/
isPointerTypedef(typeName: string): boolean {
return this.structState.pointerTypedefs.has(typeName);
}
/**
* Issue #948/#958: Check if a typedef aliases a truly opaque struct type.
* Query-time resolution: if the underlying struct tag has a body, it's not opaque.
* @param typeName Typedef name
* @returns true if the type is opaque (forward-declared with no body found)
*/
isOpaqueType(typeName: string): boolean {
if (!this.structState.opaqueTypes.has(typeName)) return false;
// Resolve: if the underlying struct tag has a body, it's not truly opaque
const tag = this.structState.typedefToTag.get(typeName);
if (tag && this.structState.structTagsWithBodies.has(tag)) return false;
return true;
}
/**
* Issue #948: Get all opaque type names for cache serialization.
* Returns the raw set — resolution happens at query time via isOpaqueType().
* @returns Array of opaque typedef names
*/
getAllOpaqueTypes(): string[] {
return Array.from(this.structState.opaqueTypes);
}
/**
* Issue #948: Register a struct tag -> typedef name relationship.
* Called when processing: typedef struct _foo foo_t;
* Populates both forward (tag→typedef) and reverse (typedef→tag) maps.
* @param structTag The struct tag name (e.g., "_foo")
* @param typedefName The typedef alias name (e.g., "foo_t")
*/
registerStructTagAlias(structTag: string, typedefName: string): void {
this.structState = produce(this.structState, (draft) => {
draft.structTagAliases.set(structTag, typedefName);
draft.typedefToTag.set(typedefName, structTag);
});
}
/**
* Issue #948: Get the typedef alias for a struct tag, if any.
* @param structTag The struct tag name
* @returns The typedef alias name, or undefined if none registered
*/
getStructTagAlias(structTag: string): string | undefined {
return this.structState.structTagAliases.get(structTag);
}
/**
* Issue #958: Record that a struct tag has a full definition (body).
* Used by query-time resolution: opaque types with bodies are not truly opaque.
* @param structTag The struct tag name (e.g., "_widget_t")
*/
markStructTagHasBody(structTag: string): void {
this.structState = produce(this.structState, (draft) => {
draft.structTagsWithBodies.add(structTag);
});
}
/**
* Issue #985: Clear a struct tag's recorded body. Used by external-declaration
* recovery to undo a PHANTOM body — one fabricated by ANTLR error-recovery when
* the normal pass parsed a header's huge preprocessed blob — so a type the
* clean per-file re-parse proves opaque (e.g. lvgl `lv_obj_t`) resolves as
* opaque again (and codegen uses a pointer).
* @param structTag The struct tag name (e.g., "_lv_obj_t")
*/
clearStructTagHasBody(structTag: string): void {
if (!this.structState.structTagsWithBodies.has(structTag)) return;
this.structState = produce(this.structState, (draft) => {
draft.structTagsWithBodies.delete(structTag);
});
}
/** Issue #985: The struct tag a typedef aliases, if any (e.g. lv_obj_t -> _lv_obj_t). */
getStructTagForTypedef(typedefName: string): string | undefined {
return this.structState.typedefToTag.get(typedefName);
}
/**
* Issue #958: Get all struct tags with bodies for cache serialization.
* @returns Array of struct tag names
*/
getAllStructTagsWithBodies(): string[] {
return Array.from(this.structState.structTagsWithBodies);
}
/**
* Issue #958: Get all struct tag aliases for cache serialization.
* @returns Array of [structTag, typedefName] pairs
*/
getAllStructTagAliases(): Array<[string, string]> {
return Array.from(this.structState.structTagAliases.entries());
}
// ========================================================================
// Issue #958: Typedef Struct Type Tracking
// ========================================================================
/**
* Issue #958: Mark a typedef as aliasing a struct type.
* Records the source file. Additive only — never removed.
* @param typedefName The typedef name (e.g., "widget_t")
* @param sourceFile The file where the typedef was declared
*/
markTypedefStructType(typedefName: string, sourceFile: string): void {
this.structState = produce(this.structState, (draft) => {
draft.typedefStructTypes.set(typedefName, sourceFile);
});
}
/**
* Issue #958: Check if a typedef aliases a struct type.
* Used for scope variables, function parameters, and local variables
* which should be pointers for C-header struct types.
*
* Issue #948: Performs query-time resolution - if the underlying struct
* tag has a full body definition, this is NOT an external typedef struct
* (it's a complete type that can use value semantics).
*
* @param typeName The type name to check
* @returns true if this is a typedef'd struct type from C headers
*/
isTypedefStructType(typeName: string): boolean {
if (!this.structState.typedefStructTypes.has(typeName)) {
return false;
}
// Issue #948: Query-time resolution - if the underlying struct tag
// has a body definition, this typedef is NOT an external struct type.
// Example: `typedef struct _point_t point_t;` followed by `struct _point_t { ... };`
// The second declaration provides the body, so point_t is a complete type.
const tag = this.structState.typedefToTag.get(typeName);
if (tag && this.structState.structTagsWithBodies.has(tag)) {
return false;
}
return true;
}
/**
* Issue #958: Get all typedef struct types for cache serialization.
* @returns Map entries as [typeName, sourceFile] pairs
*/
getAllTypedefStructTypes(): Array<[string, string]> {
return Array.from(this.structState.typedefStructTypes.entries());
}
// ========================================================================
// Enum Bit Width Tracking
// ========================================================================
/**
* 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);
}
}
// ========================================================================
// External Array Dimension Resolution
// ========================================================================
/**
* 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 {
const constValues = this.getConstValues();
if (constValues.size === 0) {
return;
}
this.resolveArrayDimensionsWithConstants(constValues);
}
/**
* Integer value of one symbol, if it is a const variable with a literal
* integer initializer.
*
* The single derivation of "what is this const worth". Issue #1220 found it
* written out twice -- here and again in CodeGenerator.initializeSymbolData()
* -- each walking getAllTSymbols() and re-deciding the kind/isConst/parse
* chain. Two copies of a rule that must agree is exactly the duplicate path
* CLAUDE.md forbids, so both callers now go through this.
*/
private constValueOfSymbol(symbol: TSymbol): number | undefined {
if (symbol.kind !== "variable" || !symbol.isConst) return undefined;
Iif (symbol.initialValue === undefined) return undefined;
return LiteralUtils.parseIntegerLiteral(symbol.initialValue);
}
/**
* Integer value of a named const, or undefined when the name is not a const
* with a literal integer initializer.
*
* Issue #1220: this is how an analyzer reaches a const that arrived through
* an #include. Before it, DivisionByZeroAnalyzer knew only the const zeros it
* had walked out of the current file's parse tree, so `10 / ZERO` with an
* imported ZERO emitted a real runtime division by zero that compiles clean
* under -Wall -Wextra.
*/
getConstValue(name: string): number | undefined {
const symbol = this.getTSymbol(name);
return symbol ? this.constValueOfSymbol(symbol) : undefined;
}
/**
* Map of every const variable name to its integer value.
*/
getConstValues(): Map<string, number> {
const constValues = new Map<string, number>();
for (const symbol of this.getAllTSymbols()) {
const value = this.constValueOfSymbol(symbol);
if (value !== undefined) {
constValues.set(symbol.name, value);
}
}
return constValues;
}
/**
* Resolve string array dimensions using const values lookup.
*/
private resolveArrayDimensionsWithConstants(
constValues: Map<string, number>,
): void {
for (const symbol of this.getAllTSymbols()) {
if (
symbol.kind === "variable" &&
symbol.isArray &&
symbol.arrayDimensions
) {
// After kind check, symbol is narrowed to IVariableSymbol
this.resolveVariableArrayDimensions(symbol, constValues);
}
}
}
/**
* Resolve array dimensions for a single variable symbol.
*/
private resolveVariableArrayDimensions(
variable: IVariableSymbol,
constValues: Map<string, number>,
): void {
let modified = false;
const resolvedDimensions = variable.arrayDimensions!.map((dim) => {
if (typeof dim === "number") {
return dim;
}
const constValue = constValues.get(dim);
Eif (constValue !== undefined) {
modified = true;
return constValue;
}
return dim;
});
if (modified) {
// Mutate in place - symbol is already in storage, cloning would require
// updating all maps. The readonly typing prevents accidental mutations
// elsewhere; this controlled mutation is intentional during resolution.
(
variable as unknown as { arrayDimensions: (number | string)[] }
).arrayDimensions = resolvedDimensions;
}
}
// ========================================================================
// Clear / Reset
// ========================================================================
/**
* Clear all symbols
*/
clear(): void {
// C-Next
this.tSymbols.clear();
this.tSymbolsByCName.clear();
this.tSymbolsByFile.clear();
// C
this.cSymbols.clear();
this.cSymbolsByFile.clear();
// C++
this.cppSymbols.clear();
this.cppSymbolsByFile.clear();
// Auxiliary
this.structFields.clear();
this.needsStructKeyword.clear();
this.structState = createInitialStructState();
this.enumBitWidth.clear();
}
}
export default SymbolTable;
|