c-next 0.2.17 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +7645 -5941
  3. package/dist/index.js.map +4 -4
  4. package/grammar/CNext.g4 +8 -0
  5. package/package.json +1 -3
  6. package/src/transpiler/Transpiler.ts +286 -26
  7. package/src/transpiler/__tests__/compileCommandsDiscovery.integration.test.ts +94 -0
  8. package/src/transpiler/__tests__/externalSymbolRecovery.integration.test.ts +215 -0
  9. package/src/transpiler/logic/__tests__/detectAssemblySyntax.test.ts +116 -0
  10. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +8 -0
  11. package/src/transpiler/logic/analysis/MixedTypeCategoryAnalyzer.ts +408 -0
  12. package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +16 -97
  13. package/src/transpiler/logic/analysis/ReturnPathAnalyzer.ts +171 -0
  14. package/src/transpiler/logic/analysis/__tests__/MixedTypeCategoryAnalyzer.test.ts +346 -0
  15. package/src/transpiler/logic/analysis/__tests__/ReturnPathAnalyzer.test.ts +232 -0
  16. package/src/transpiler/logic/analysis/runAnalyzers.ts +23 -2
  17. package/src/transpiler/logic/analysis/types/IMixedTypeCategoryError.ts +17 -0
  18. package/src/transpiler/logic/analysis/types/IReturnPathError.ts +12 -0
  19. package/src/transpiler/logic/detectAssemblySyntax.ts +80 -0
  20. package/src/transpiler/logic/parser/grammar/CNext.interp +4 -1
  21. package/src/transpiler/logic/parser/grammar/CNext.tokens +169 -167
  22. package/src/transpiler/logic/parser/grammar/CNextLexer.interp +4 -1
  23. package/src/transpiler/logic/parser/grammar/CNextLexer.tokens +169 -167
  24. package/src/transpiler/logic/parser/grammar/CNextLexer.ts +545 -541
  25. package/src/transpiler/logic/parser/grammar/CNextListener.ts +11 -0
  26. package/src/transpiler/logic/parser/grammar/CNextParser.ts +1257 -1185
  27. package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +7 -0
  28. package/src/transpiler/logic/preprocessor/CompileCommandsReader.ts +332 -0
  29. package/src/transpiler/logic/preprocessor/ExternalDeclarationOracle.ts +202 -0
  30. package/src/transpiler/logic/preprocessor/Preprocessor.ts +24 -6
  31. package/src/transpiler/logic/preprocessor/ToolchainDetector.ts +42 -1
  32. package/src/transpiler/logic/preprocessor/__tests__/CompileCommandsReader.test.ts +216 -0
  33. package/src/transpiler/logic/preprocessor/__tests__/ExternalDeclarationOracle.test.ts +153 -0
  34. package/src/transpiler/logic/preprocessor/__tests__/Preprocessor.test.ts +43 -18
  35. package/src/transpiler/logic/preprocessor/__tests__/ToolchainDetector.crossCompiler.test.ts +75 -0
  36. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/dependent.h +7 -0
  37. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/predecessor.h +5 -0
  38. package/src/transpiler/logic/preprocessor/types/ICompileCommandsResult.ts +14 -0
  39. package/src/transpiler/logic/preprocessor/types/IPreprocessOptions.ts +17 -0
  40. package/src/transpiler/logic/symbols/SymbolTable.ts +45 -0
  41. package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +49 -0
  42. package/src/transpiler/output/MisraSuppressionUtils.ts +52 -0
  43. package/src/transpiler/output/__tests__/MisraSuppressionUtils.test.ts +67 -0
  44. package/src/transpiler/output/codegen/CodeGenerator.ts +45 -4
  45. package/src/transpiler/output/codegen/TypeResolver.ts +148 -0
  46. package/src/transpiler/output/codegen/TypeValidator.ts +179 -81
  47. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +13 -1
  48. package/src/transpiler/output/codegen/__tests__/TypeResolver.test.ts +93 -0
  49. package/src/transpiler/output/codegen/__tests__/TypeValidator.alwaysTrueLoop.test.ts +91 -0
  50. package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +86 -14
  51. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +13 -0
  52. package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +1 -1
  53. package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +20 -12
  54. package/src/transpiler/output/codegen/assignment/handlers/ArrayHandlers.ts +424 -22
  55. package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +31 -18
  56. package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +7 -8
  57. package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +6 -8
  58. package/src/transpiler/output/codegen/assignment/handlers/RegisterUtils.ts +12 -10
  59. package/src/transpiler/output/codegen/assignment/handlers/SimpleHandler.ts +3 -7
  60. package/src/transpiler/output/codegen/assignment/handlers/SpecialHandlers.ts +7 -9
  61. package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +12 -10
  62. package/src/transpiler/output/codegen/assignment/handlers/__tests__/ArrayHandlers.test.ts +479 -11
  63. package/src/transpiler/output/codegen/generators/IOrchestrator.ts +3 -0
  64. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +28 -15
  65. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +26 -13
  66. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +129 -1
  67. package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +64 -8
  68. package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +8 -5
  69. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +30 -2
  70. package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +16 -13
  71. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +19 -0
  72. package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +57 -11
  73. package/src/transpiler/output/codegen/subscript/SubscriptClassifier.ts +30 -11
  74. package/src/transpiler/output/codegen/subscript/TSubscriptKind.ts +1 -1
  75. package/src/transpiler/output/codegen/subscript/__tests__/SubscriptClassifier.test.ts +38 -6
  76. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +65 -24
  77. package/src/transpiler/state/CodeGenState.ts +20 -16
  78. package/src/transpiler/types/ICachedFileEntry.ts +7 -0
  79. package/src/utils/cache/CacheManager.ts +13 -2
  80. package/src/utils/cache/__tests__/CacheManager.test.ts +18 -1
  81. package/src/utils/constants/TypeConstants.ts +13 -0
package/grammar/CNext.g4 CHANGED
@@ -217,6 +217,7 @@ statement
217
217
  | whileStatement
218
218
  | doWhileStatement
219
219
  | forStatement
220
+ | foreverStatement // ADR-113: Infinite loops
220
221
  | switchStatement
221
222
  | returnStatement
222
223
  | criticalStatement // ADR-050: Critical sections
@@ -283,6 +284,12 @@ forStatement
283
284
  : 'for' '(' forInit? ';' expression? ';' forUpdate? ')' statement
284
285
  ;
285
286
 
287
+ // ADR-113: Infinite loop. Braces always required (no single-statement form),
288
+ // matching switch/scope/critical. Lowers to MISRA-compliant for(;;).
289
+ foreverStatement
290
+ : FOREVER block
291
+ ;
292
+
286
293
  // For loop init - uses versions without trailing semicolons
287
294
  forInit
288
295
  : forVarDecl
@@ -642,6 +649,7 @@ ELSE : 'else';
642
649
  WHILE : 'while';
643
650
  DO : 'do'; // ADR-027: Do-while loops
644
651
  FOR : 'for';
652
+ FOREVER : 'forever'; // ADR-113: Infinite loops
645
653
  SWITCH : 'switch'; // ADR-025: Switch statements
646
654
  CASE : 'case';
647
655
  DEFAULT : 'default';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
5
  "packageManager": "npm@11.9.0",
6
6
  "type": "module",
@@ -23,11 +23,9 @@
23
23
  "test:q": "tsx scripts/test.ts -q",
24
24
  "test:update": "tsx scripts/test.ts --update",
25
25
  "integration:transpile": "tsx scripts/test.ts --transpile-only",
26
- "integration:execute": "tsx scripts/test.ts --execute-only",
27
26
  "test:bugs": "tsx scripts/test.ts -- bugs",
28
27
  "test:bugs:q": "tsx scripts/test.ts -- bugs -q",
29
28
  "integration:bugs:transpile": "tsx scripts/test.ts -- bugs --transpile-only",
30
- "integration:bugs:execute": "tsx scripts/test.ts -- bugs --execute-only",
31
29
  "analyze": "./scripts/static-analysis.sh",
32
30
  "clean": "rm -rf dist src/transpiler/logic/parser/grammar src/transpiler/logic/parser/c/grammar src/transpiler/logic/parser/cpp/grammar",
33
31
  "prettier:check": "prettier --check .",
@@ -36,6 +36,10 @@ import HeaderSymbolAdapter from "./output/headers/adapters/HeaderSymbolAdapter";
36
36
  import IHeaderSymbol from "./output/headers/types/IHeaderSymbol";
37
37
  import TSymbol from "./types/symbols/TSymbol";
38
38
  import Preprocessor from "./logic/preprocessor/Preprocessor";
39
+ import ToolchainDetector from "./logic/preprocessor/ToolchainDetector";
40
+ import CompileCommandsReader from "./logic/preprocessor/CompileCommandsReader";
41
+ import IToolchain from "./logic/preprocessor/types/IToolchain";
42
+ import ICompileCommandsResult from "./logic/preprocessor/types/ICompileCommandsResult";
39
43
 
40
44
  import FileDiscovery from "./data/FileDiscovery";
41
45
  import EFileType from "./data/types/EFileType";
@@ -62,6 +66,8 @@ import ModificationAnalyzer from "./logic/analysis/ModificationAnalyzer";
62
66
  import CacheManager from "../utils/cache/CacheManager";
63
67
  import MapUtils from "../utils/MapUtils";
64
68
  import detectCppSyntax from "./logic/detectCppSyntax";
69
+ import detectAssemblySyntax from "./logic/detectAssemblySyntax";
70
+ import ExternalDeclarationOracle from "./logic/preprocessor/ExternalDeclarationOracle";
65
71
  import TransitiveEnumCollector from "./logic/symbols/TransitiveEnumCollector";
66
72
  import TypedefParamParser from "./output/codegen/helpers/TypedefParamParser";
67
73
 
@@ -77,6 +83,13 @@ class Transpiler {
77
83
  private readonly cacheManager: CacheManager | null;
78
84
  /** Issue #211: Tracks if C++ output is needed (one-way flag, false → true only) */
79
85
  private cppDetected: boolean;
86
+
87
+ /**
88
+ * Set when any C header failed standalone preprocessing (fell back to raw
89
+ * text). Gates the ExternalDeclarationOracle recovery pass (Issue #985) so
90
+ * only projects with unresolvable framework headers pay its cost.
91
+ */
92
+ private anyHeaderPreprocessFailed = false;
80
93
  /** Issue #587: Encapsulated state for accumulated Maps/Sets */
81
94
  private readonly state = new TranspilerState();
82
95
  /**
@@ -112,7 +125,24 @@ class Transpiler {
112
125
  // Issue #211: Initialize cppDetected from config (--cpp flag sets this)
113
126
  this.cppDetected = this.config.cppRequired;
114
127
 
115
- this.preprocessor = new Preprocessor();
128
+ // Adopt the compiler's own view from the project's compile_commands.json, if
129
+ // present. Every build system (CMake, PlatformIO, Meson, Zephyr, bear-wrapped
130
+ // Make) emits this database; reading it — rather than mirroring framework
131
+ // include paths in cnext.config.json — lets cnext resolve external headers
132
+ // exactly as the compiler will, which is the same reason clangd reads it.
133
+ // The include paths + defines + compiler are the contract every build system
134
+ // converges on. (Issue #985 external-symbol recovery; unblocks ADR-062.)
135
+ const projectRoot = this.determineProjectRoot();
136
+ const compileDb = projectRoot
137
+ ? CompileCommandsReader.load(join(projectRoot, "compile_commands.json"))
138
+ : null;
139
+ if (compileDb) {
140
+ this._applyCompileCommands(compileDb);
141
+ }
142
+
143
+ this.preprocessor = new Preprocessor(
144
+ Transpiler._toolchainForCompileDb(compileDb),
145
+ );
116
146
  this.codeGenerator = new CodeGenerator();
117
147
  this.headerGenerator = new HeaderGenerator();
118
148
  this.warnings = [];
@@ -128,13 +158,44 @@ class Transpiler {
128
158
  this.fs,
129
159
  );
130
160
 
131
- // Initialize cache manager if caching is enabled and project root can be determined
132
- const projectRoot = this.config.noCache
133
- ? undefined
134
- : this.determineProjectRoot();
135
- this.cacheManager = projectRoot
136
- ? new CacheManager(projectRoot, this.fs)
137
- : null;
161
+ // Initialize cache manager if caching is enabled and a project root was found.
162
+ this.cacheManager =
163
+ !this.config.noCache && projectRoot
164
+ ? new CacheManager(projectRoot, this.fs)
165
+ : null;
166
+ }
167
+
168
+ /**
169
+ * Merge a discovered compile_commands.json into the effective config: union its
170
+ * include search paths into includeDirs (so both preprocessing and include-tree
171
+ * resolution see what the compiler sees) and merge its defines beneath the
172
+ * explicit CLI/config defines, which win on conflict.
173
+ */
174
+ private _applyCompileCommands(db: ICompileCommandsResult): void {
175
+ const merged = [...this.config.includeDirs];
176
+ const seen = new Set(merged);
177
+ for (const path of db.includePaths) {
178
+ if (!seen.has(path)) {
179
+ seen.add(path);
180
+ merged.push(path);
181
+ }
182
+ }
183
+ this.config.includeDirs = merged;
184
+ this.config.defines = { ...db.defines, ...this.config.defines };
185
+ }
186
+
187
+ /**
188
+ * The toolchain to preprocess with, given a discovered compile database. An
189
+ * explicit CNEXT_CROSS_COMPILER override always wins (deferred to Preprocessor's
190
+ * own detection); otherwise adopt the database's compiler if it resolves, else
191
+ * fall back to auto-detection.
192
+ */
193
+ private static _toolchainForCompileDb(
194
+ db: ICompileCommandsResult | null,
195
+ ): IToolchain | undefined {
196
+ if (process.env.CNEXT_CROSS_COMPILER) return undefined;
197
+ if (!db?.compiler) return undefined;
198
+ return ToolchainDetector.fromPath(db.compiler) ?? undefined;
138
199
  }
139
200
 
140
201
  // ===========================================================================
@@ -218,6 +279,16 @@ class Transpiler {
218
279
  // Stage 2: Collect symbols from C/C++ headers and build analyzer context
219
280
  // Issue #945: Now async for preprocessing support
220
281
  await this._collectAllHeaderSymbols(input.headerFiles, result);
282
+
283
+ // Issue #985 recovery: when standalone header preprocessing missed framework
284
+ // symbols, recover their declared names via translation-unit preprocessing.
285
+ await this._collectExternalDeclarations(input);
286
+
287
+ // Snapshot external struct fields for InitializationAnalyzer AFTER recovery so
288
+ // structs that only become known through #985 recovery (their fields are added
289
+ // to symbolTable by _collectExternalDeclarations) are folded in and remain
290
+ // subject to init-completeness checking. Nothing consumes externalStructFields
291
+ // before Stage 5, so a single post-recovery build is sufficient.
221
292
  CodeGenState.buildExternalStructFields();
222
293
 
223
294
  // Stage 3: Collect symbols from C-Next files
@@ -628,13 +699,146 @@ class Transpiler {
628
699
  headerFiles: IDiscoveredFile[],
629
700
  result: ITranspilerResult,
630
701
  ): Promise<void> {
702
+ const precedingHeaders: string[] = [];
631
703
  for (const file of headerFiles) {
704
+ let usable = false;
632
705
  try {
633
- await this.doCollectHeaderSymbols(file);
706
+ usable = await this.doCollectHeaderSymbols(file, precedingHeaders);
634
707
  result.filesProcessed++;
635
708
  } catch (err) {
636
709
  this.warnings.push(`Failed to process header ${file.path}: ${err}`);
637
710
  }
711
+ // Offer this header as macro context to headers processed after it, but
712
+ // only if it itself preprocessed cleanly — an unpreprocessable predecessor
713
+ // would otherwise make every dependent's -imacros retry fail.
714
+ if (usable) {
715
+ precedingHeaders.push(file.path);
716
+ }
717
+ }
718
+ }
719
+
720
+ /**
721
+ * Issue #985 recovery: recover the NAMES of framework functions / function-like
722
+ * macros that standalone header preprocessing missed, by preprocessing each
723
+ * .cnx's C includes as a translation unit (predecessors first — the way the
724
+ * real compiler does). Requires a toolchain that can preprocess the target's
725
+ * headers; for cross targets set CNEXT_CROSS_COMPILER. Gated on a preprocess
726
+ * failure so clean projects pay nothing.
727
+ */
728
+ private async _collectExternalDeclarations(
729
+ input: IPipelineInput,
730
+ ): Promise<void> {
731
+ if (!this.anyHeaderPreprocessFailed) return;
732
+
733
+ const directives = this._collectCIncludeDirectives(input);
734
+ if (directives.length === 0) return;
735
+
736
+ const recovery = await ExternalDeclarationOracle.recover(
737
+ directives,
738
+ this.preprocessor,
739
+ { includePaths: this.config.includeDirs, defines: this.config.defines },
740
+ );
741
+ if (!recovery) return;
742
+
743
+ const cleanState = this._parseRecoveredSlices(recovery.perFileContent);
744
+ Transpiler._clearPhantomStructBodies(cleanState);
745
+
746
+ // Function-like macros have no declaration to parse; register their names for
747
+ // the undeclared-call check only (a by-value macro invocation is correct).
748
+ if (recovery.macroNames.size > 0) {
749
+ CodeGenState.symbolTable.addExternalDeclarationNames(recovery.macroNames);
750
+ }
751
+ }
752
+
753
+ /** Every C header the .cnx files include, deduped in first-seen source order. */
754
+ private _collectCIncludeDirectives(input: IPipelineInput): string[] {
755
+ const seen = new Set<string>();
756
+ const directives: string[] = [];
757
+ for (const file of input.cnextFiles) {
758
+ const source = file.source ?? this.readFileOrEmpty(file.path);
759
+ for (const directive of Transpiler.extractCIncludeDirectives(source)) {
760
+ if (!seen.has(directive)) {
761
+ seen.add(directive);
762
+ directives.push(directive);
763
+ }
764
+ }
765
+ }
766
+ return directives;
767
+ }
768
+
769
+ /**
770
+ * Parse each header's own preprocessed slice with the real header parser so
771
+ * recovered symbols carry FULL types — function signatures, typedefs, opaque
772
+ * structs — not just names. Each slice is macro-expanded (so e.g. FreeRTOS
773
+ * PRIVILEGED_FUNCTION is gone and vTaskDelay parses) yet small (no inlined
774
+ * tree, so ANTLR error-recovery doesn't drop declarations). Codegen needs
775
+ * these to pass structs by address (twai_driver_install(&cfg)) and treat
776
+ * opaque framework types as pointers (lv_obj_t -> lv_obj_t*).
777
+ *
778
+ * A second, isolated table is parsed in parallel and returned: it is clean of
779
+ * the normal pass's degraded-blob data, so it holds the AUTHORITATIVE
780
+ * opaque/struct-body truth. parseCHeader (main table) auto-detects C vs C++ and
781
+ * skips assembler; the isolated table uses the C parser directly (opaque struct
782
+ * typedefs are a C concern) and tolerates slices it cannot parse.
783
+ */
784
+ private _parseRecoveredSlices(
785
+ perFileContent: Map<string, string>,
786
+ ): SymbolTable {
787
+ const cleanState = new SymbolTable();
788
+ for (const [path, content] of perFileContent) {
789
+ try {
790
+ this.parseCHeader(content, path);
791
+ } catch {
792
+ // A slice that won't parse leaves the (already-collected) symbols as they
793
+ // were — skip it rather than fail the build.
794
+ }
795
+ const { tree } = HeaderParser.parseC(content);
796
+ if (!tree) continue;
797
+ try {
798
+ CResolver.resolve(tree, path, cleanState);
799
+ } catch {
800
+ /* isolated best-effort — only its opaque/body verdict is consulted */
801
+ }
802
+ }
803
+ return cleanState;
804
+ }
805
+
806
+ /**
807
+ * Undo PHANTOM struct bodies: when the normal pass parsed a header's huge
808
+ * preprocessed blob, ANTLR error-recovery could fabricate a `struct X { ... }`
809
+ * that was never really there (e.g. lvgl `struct _lv_obj_t`), which makes an
810
+ * opaque typedef look complete and defeats pointer codegen. The clean per-file
811
+ * re-parse (`cleanState`) is authoritative, so for every type it proves opaque,
812
+ * clear any body its tag does NOT actually have.
813
+ */
814
+ private static _clearPhantomStructBodies(cleanState: SymbolTable): void {
815
+ const cleanBodies = new Set(cleanState.getAllStructTagsWithBodies());
816
+ for (const typedefName of cleanState.getAllOpaqueTypes()) {
817
+ if (!cleanState.isOpaqueType(typedefName)) continue;
818
+ const tag = CodeGenState.symbolTable.getStructTagForTypedef(typedefName);
819
+ if (tag && !cleanBodies.has(tag)) {
820
+ CodeGenState.symbolTable.clearStructTagHasBody(tag);
821
+ }
822
+ }
823
+ }
824
+
825
+ /** Extract C header include directives (`<...>` / `"..."`, non-.cnx) in order. */
826
+ private static extractCIncludeDirectives(source: string): string[] {
827
+ const directives: string[] = [];
828
+ const re = /^[ \t]*#include\s+([<"][^>"]+[>"])/gm;
829
+ for (const match of source.matchAll(re)) {
830
+ const spec = match[1];
831
+ if (/\.cnx[>"]$/.test(spec)) continue; // C-Next include, not a C header
832
+ directives.push(spec);
833
+ }
834
+ return directives;
835
+ }
836
+
837
+ private readFileOrEmpty(path: string): string {
838
+ try {
839
+ return this.fs.readFile(path);
840
+ } catch {
841
+ return "";
638
842
  }
639
843
  }
640
844
 
@@ -1058,18 +1262,25 @@ class Transpiler {
1058
1262
  * Issue #945: Added preprocessing support for conditional compilation
1059
1263
  * SonarCloud S3776: Refactored to use helper methods for reduced complexity.
1060
1264
  */
1061
- private async doCollectHeaderSymbols(file: IDiscoveredFile): Promise<void> {
1265
+ private async doCollectHeaderSymbols(
1266
+ file: IDiscoveredFile,
1267
+ precedingHeaders: readonly string[] = [],
1268
+ ): Promise<boolean> {
1062
1269
  // Track as processed (for cycle detection)
1063
1270
  const absolutePath = resolve(file.path);
1064
1271
  this.state.markHeaderProcessed(absolutePath);
1065
1272
 
1066
1273
  // Check cache first
1067
- if (this.tryRestoreFromCache(file)) {
1068
- return; // Cache hit - skip full parsing
1274
+ const restored = this.tryRestoreFromCache(file);
1275
+ if (restored) {
1276
+ return restored.usable; // Cache hit - skip full parsing
1069
1277
  }
1070
1278
 
1071
1279
  // Issue #945: Preprocess header to evaluate #if/#ifdef directives
1072
- const content = await this.getHeaderContent(file);
1280
+ const { content, usable } = await this.getHeaderContent(
1281
+ file,
1282
+ precedingHeaders,
1283
+ );
1073
1284
  this.parseHeaderFile(file, content);
1074
1285
 
1075
1286
  // Debug: Show symbols found
@@ -1078,27 +1289,34 @@ class Transpiler {
1078
1289
  console.log(`[DEBUG] Found ${symbols.length} symbols in ${file.path}`);
1079
1290
  }
1080
1291
 
1081
- // Issue #590: Cache the results using simplified API
1292
+ // Issue #590: Cache the results using simplified API. Issue #985: record when
1293
+ // this header fell back to raw content so a warm-cache build re-runs recovery.
1082
1294
  if (this.cacheManager) {
1083
1295
  this.cacheManager.setSymbolsFromTable(
1084
1296
  file.path,
1085
1297
  CodeGenState.symbolTable,
1298
+ !usable,
1086
1299
  );
1087
1300
  }
1301
+
1302
+ return usable;
1088
1303
  }
1089
1304
 
1090
1305
  /**
1091
- * Try to restore symbols from cache. Returns true if cache hit.
1306
+ * Try to restore symbols from cache. Returns the restored header's usability
1307
+ * (whether it preprocessed cleanly) on a cache hit, or null on a miss.
1092
1308
  * SonarCloud S3776: Extracted from doCollectHeaderSymbols().
1093
1309
  */
1094
- private tryRestoreFromCache(file: IDiscoveredFile): boolean {
1310
+ private tryRestoreFromCache(
1311
+ file: IDiscoveredFile,
1312
+ ): { usable: boolean } | null {
1095
1313
  if (!this.cacheManager?.isValid(file.path)) {
1096
- return false;
1314
+ return null;
1097
1315
  }
1098
1316
 
1099
1317
  const cached = this.cacheManager.getSymbols(file.path);
1100
1318
  if (!cached) {
1101
- return false;
1319
+ return null;
1102
1320
  }
1103
1321
 
1104
1322
  // Restore symbols, struct fields, needsStructKeyword, and enumBitWidth from cache
@@ -1127,7 +1345,15 @@ class Transpiler {
1127
1345
  // Issue #211: Still check for C++ syntax even on cache hit
1128
1346
  this.detectCppFromFileType(file);
1129
1347
 
1130
- return true;
1348
+ // Issue #985: The cached symbols of a header that fell back to raw content
1349
+ // are degraded. Re-arm the recovery gate so a warm-cache build still runs
1350
+ // the external-declaration recovery pass and re-applies its corrections to
1351
+ // the in-memory symbol table (the cache itself holds the degraded symbols).
1352
+ if (cached.preprocessFailed) {
1353
+ this.anyHeaderPreprocessFailed = true;
1354
+ }
1355
+
1356
+ return { usable: !cached.preprocessFailed };
1131
1357
  }
1132
1358
 
1133
1359
  /**
@@ -1138,24 +1364,27 @@ class Transpiler {
1138
1364
  * Preprocessing is needed when the file has conditional compilation patterns
1139
1365
  * like #if MACRO != 0 that require expression evaluation.
1140
1366
  */
1141
- private async getHeaderContent(file: IDiscoveredFile): Promise<string> {
1367
+ private async getHeaderContent(
1368
+ file: IDiscoveredFile,
1369
+ precedingHeaders: readonly string[] = [],
1370
+ ): Promise<{ content: string; usable: boolean }> {
1142
1371
  const rawContent = this.fs.readFile(file.path);
1143
1372
 
1144
1373
  // Check if preprocessing is disabled
1145
1374
  if (this.config.preprocess === false) {
1146
- return rawContent;
1375
+ return { content: rawContent, usable: true };
1147
1376
  }
1148
1377
 
1149
1378
  // Check if preprocessing is available
1150
1379
  if (!this.preprocessor.isAvailable()) {
1151
- return rawContent;
1380
+ return { content: rawContent, usable: true };
1152
1381
  }
1153
1382
 
1154
1383
  // Issue #945: Only preprocess if file has conditional compilation patterns
1155
1384
  // that require expression evaluation (e.g., #if MACRO != 0, #if MACRO == 1)
1156
1385
  // Simple #ifdef/#ifndef patterns are already handled by the parser
1157
1386
  if (!this.needsConditionalPreprocessing(rawContent)) {
1158
- return rawContent;
1387
+ return { content: rawContent, usable: true };
1159
1388
  }
1160
1389
 
1161
1390
  // Preprocess the header file
@@ -1166,14 +1395,34 @@ class Transpiler {
1166
1395
  });
1167
1396
 
1168
1397
  if (!result.success) {
1169
- // Log warning but fall back to raw content
1398
+ // Some headers cannot be preprocessed standalone: they require a
1399
+ // predecessor to have run first (e.g. FreeRTOS task.h needs FreeRTOS.h to
1400
+ // define INC_FREERTOS_H and its attribute macros, and enforces this with
1401
+ // its own #error). Retry importing the macros of the headers collected
1402
+ // before this one (only those that themselves preprocessed cleanly, so one
1403
+ // unpreprocessable predecessor can't defeat the retry).
1404
+ if (precedingHeaders.length > 0) {
1405
+ const retry = await this.preprocessor.preprocess(file.path, {
1406
+ defines: this.config.defines,
1407
+ includePaths: this.config.includeDirs,
1408
+ keepLineDirectives: false,
1409
+ imacros: [...precedingHeaders],
1410
+ });
1411
+ if (retry.success) {
1412
+ return { content: retry.content, usable: true };
1413
+ }
1414
+ }
1415
+ // Fall back to raw content. Mark not-usable so this header is not offered
1416
+ // as macro context to headers processed after it, and flag that TU-level
1417
+ // external-declaration recovery is warranted (Issue #985).
1418
+ this.anyHeaderPreprocessFailed = true;
1170
1419
  this.warnings.push(
1171
1420
  `Preprocessing failed for ${file.path}: ${result.error}. Using raw content.`,
1172
1421
  );
1173
- return rawContent;
1422
+ return { content: rawContent, usable: false };
1174
1423
  }
1175
1424
 
1176
- return result.content;
1425
+ return { content: result.content, usable: true };
1177
1426
  }
1178
1427
 
1179
1428
  /**
@@ -1323,6 +1572,17 @@ class Transpiler {
1323
1572
  * Uses heuristic detection to choose the appropriate parser
1324
1573
  */
1325
1574
  private parseCHeader(content: string, filePath: string): void {
1575
+ // Assembler headers (e.g. xtensa coreasm.h, pulled in transitively by
1576
+ // FreeRTOS port headers) are not C. Parsing their `.macro` bodies as C
1577
+ // mis-collects instruction mnemonics like `loop` as C symbols that then
1578
+ // false-conflict with C-Next symbols of the same name. Skip them entirely.
1579
+ if (detectAssemblySyntax(content)) {
1580
+ if (this.config.debugMode) {
1581
+ console.log(`[DEBUG] Skipping assembler header: ${filePath}`);
1582
+ }
1583
+ return;
1584
+ }
1585
+
1326
1586
  if (detectCppSyntax(content)) {
1327
1587
  // Issue #211: C++ detected, set flag for .cpp output
1328
1588
  this.cppDetected = true;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Integration test: auto-discovery of compile_commands.json.
3
+ *
4
+ * cnext resolves external C/C++ headers exactly as the compiler will by reading
5
+ * the build-system-agnostic contract every build system emits — the
6
+ * `compile_commands.json` compilation database — from the project root, the way
7
+ * clangd does. This proves the wiring end-to-end: a header reachable ONLY through
8
+ * the compile database's `-I` (never passed via includeDirs / cnext.config.json)
9
+ * must resolve during a real transpile, and its pointer-parameter signature must
10
+ * drive `&` codegen.
11
+ *
12
+ * Requires a C preprocessor toolchain; skips gracefully when none is available.
13
+ */
14
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
15
+ import {
16
+ mkdtempSync,
17
+ writeFileSync,
18
+ mkdirSync,
19
+ rmSync,
20
+ readFileSync,
21
+ } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import Transpiler from "../Transpiler";
25
+ import Preprocessor from "../logic/preprocessor/Preprocessor";
26
+
27
+ const WIDGET_H = `typedef struct { int mode; } widget_cfg_t;
28
+ void widget_do(const widget_cfg_t *cfg);
29
+ `;
30
+
31
+ const MAIN_CNX = `#include <widget.h>
32
+
33
+ scope Demo {
34
+ void run() {
35
+ widget_cfg_t cfg <- {mode: 1};
36
+ global.widget_do(cfg);
37
+ }
38
+ }
39
+ `;
40
+
41
+ describe("compile_commands.json auto-discovery (integration)", () => {
42
+ let dir: string;
43
+ const available = new Preprocessor().isAvailable();
44
+
45
+ beforeAll(() => {
46
+ dir = mkdtempSync(join(tmpdir(), "cnext-cc-discovery-"));
47
+ mkdirSync(join(dir, "ext"));
48
+ writeFileSync(join(dir, "ext", "widget.h"), WIDGET_H);
49
+ writeFileSync(join(dir, "main.cnx"), MAIN_CNX);
50
+ // Project-root marker so discovery locates the database.
51
+ writeFileSync(join(dir, "cnext.config.json"), "{}\n");
52
+ // The compiler's own view: <widget.h> is reachable ONLY via this -I.
53
+ writeFileSync(
54
+ join(dir, "compile_commands.json"),
55
+ JSON.stringify([
56
+ {
57
+ directory: dir,
58
+ file: join(dir, "main.cpp"),
59
+ command: `cc -I${join(dir, "ext")} -c main.cpp`,
60
+ },
61
+ ]),
62
+ );
63
+ });
64
+
65
+ afterAll(() => {
66
+ if (dir) rmSync(dir, { recursive: true, force: true });
67
+ });
68
+
69
+ it("resolves a header supplied only by the compile database", async () => {
70
+ if (!available) return; // no toolchain in this env
71
+
72
+ // Note: NO includeDirs — the ext/ path exists solely in compile_commands.json.
73
+ const transpiler = new Transpiler({
74
+ input: join(dir, "main.cnx"),
75
+ outDir: join(dir, "out"),
76
+ cppRequired: true,
77
+ noCache: true,
78
+ });
79
+
80
+ const result = await transpiler.transpile({ kind: "files" });
81
+
82
+ const unresolved = result.errors.find((e) =>
83
+ e.message.includes("widget_do"),
84
+ );
85
+ expect(unresolved).toBeUndefined();
86
+ expect(result.success).toBe(true);
87
+
88
+ const generated = result.outputFiles.find((f) => f.endsWith("main.cpp"));
89
+ expect(generated).toBeDefined();
90
+ const code = readFileSync(generated!, "utf8");
91
+ // Pointer-parameter signature (from the compile-db-supplied header) -> `&`.
92
+ expect(code).toMatch(/widget_do\(\s*&/);
93
+ });
94
+ });