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.
- package/README.md +2 -2
- package/dist/index.js +7645 -5941
- package/dist/index.js.map +4 -4
- package/grammar/CNext.g4 +8 -0
- package/package.json +1 -3
- package/src/transpiler/Transpiler.ts +286 -26
- package/src/transpiler/__tests__/compileCommandsDiscovery.integration.test.ts +94 -0
- package/src/transpiler/__tests__/externalSymbolRecovery.integration.test.ts +215 -0
- package/src/transpiler/logic/__tests__/detectAssemblySyntax.test.ts +116 -0
- package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +8 -0
- package/src/transpiler/logic/analysis/MixedTypeCategoryAnalyzer.ts +408 -0
- package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +16 -97
- package/src/transpiler/logic/analysis/ReturnPathAnalyzer.ts +171 -0
- package/src/transpiler/logic/analysis/__tests__/MixedTypeCategoryAnalyzer.test.ts +346 -0
- package/src/transpiler/logic/analysis/__tests__/ReturnPathAnalyzer.test.ts +232 -0
- package/src/transpiler/logic/analysis/runAnalyzers.ts +23 -2
- package/src/transpiler/logic/analysis/types/IMixedTypeCategoryError.ts +17 -0
- package/src/transpiler/logic/analysis/types/IReturnPathError.ts +12 -0
- package/src/transpiler/logic/detectAssemblySyntax.ts +80 -0
- package/src/transpiler/logic/parser/grammar/CNext.interp +4 -1
- package/src/transpiler/logic/parser/grammar/CNext.tokens +169 -167
- package/src/transpiler/logic/parser/grammar/CNextLexer.interp +4 -1
- package/src/transpiler/logic/parser/grammar/CNextLexer.tokens +169 -167
- package/src/transpiler/logic/parser/grammar/CNextLexer.ts +545 -541
- package/src/transpiler/logic/parser/grammar/CNextListener.ts +11 -0
- package/src/transpiler/logic/parser/grammar/CNextParser.ts +1257 -1185
- package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +7 -0
- package/src/transpiler/logic/preprocessor/CompileCommandsReader.ts +332 -0
- package/src/transpiler/logic/preprocessor/ExternalDeclarationOracle.ts +202 -0
- package/src/transpiler/logic/preprocessor/Preprocessor.ts +24 -6
- package/src/transpiler/logic/preprocessor/ToolchainDetector.ts +42 -1
- package/src/transpiler/logic/preprocessor/__tests__/CompileCommandsReader.test.ts +216 -0
- package/src/transpiler/logic/preprocessor/__tests__/ExternalDeclarationOracle.test.ts +153 -0
- package/src/transpiler/logic/preprocessor/__tests__/Preprocessor.test.ts +43 -18
- package/src/transpiler/logic/preprocessor/__tests__/ToolchainDetector.crossCompiler.test.ts +75 -0
- package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/dependent.h +7 -0
- package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/predecessor.h +5 -0
- package/src/transpiler/logic/preprocessor/types/ICompileCommandsResult.ts +14 -0
- package/src/transpiler/logic/preprocessor/types/IPreprocessOptions.ts +17 -0
- package/src/transpiler/logic/symbols/SymbolTable.ts +45 -0
- package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +49 -0
- package/src/transpiler/output/MisraSuppressionUtils.ts +52 -0
- package/src/transpiler/output/__tests__/MisraSuppressionUtils.test.ts +67 -0
- package/src/transpiler/output/codegen/CodeGenerator.ts +45 -4
- package/src/transpiler/output/codegen/TypeResolver.ts +148 -0
- package/src/transpiler/output/codegen/TypeValidator.ts +179 -81
- package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +13 -1
- package/src/transpiler/output/codegen/__tests__/TypeResolver.test.ts +93 -0
- package/src/transpiler/output/codegen/__tests__/TypeValidator.alwaysTrueLoop.test.ts +91 -0
- package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +86 -14
- package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +13 -0
- package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +1 -1
- package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +20 -12
- package/src/transpiler/output/codegen/assignment/handlers/ArrayHandlers.ts +424 -22
- package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +31 -18
- package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +7 -8
- package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +6 -8
- package/src/transpiler/output/codegen/assignment/handlers/RegisterUtils.ts +12 -10
- package/src/transpiler/output/codegen/assignment/handlers/SimpleHandler.ts +3 -7
- package/src/transpiler/output/codegen/assignment/handlers/SpecialHandlers.ts +7 -9
- package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +12 -10
- package/src/transpiler/output/codegen/assignment/handlers/__tests__/ArrayHandlers.test.ts +479 -11
- package/src/transpiler/output/codegen/generators/IOrchestrator.ts +3 -0
- package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +28 -15
- package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +26 -13
- package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +129 -1
- package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +64 -8
- package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +8 -5
- package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +30 -2
- package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +16 -13
- package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +19 -0
- package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +57 -11
- package/src/transpiler/output/codegen/subscript/SubscriptClassifier.ts +30 -11
- package/src/transpiler/output/codegen/subscript/TSubscriptKind.ts +1 -1
- package/src/transpiler/output/codegen/subscript/__tests__/SubscriptClassifier.test.ts +38 -6
- package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +65 -24
- package/src/transpiler/state/CodeGenState.ts +20 -16
- package/src/transpiler/types/ICachedFileEntry.ts +7 -0
- package/src/utils/cache/CacheManager.ts +13 -2
- package/src/utils/cache/__tests__/CacheManager.test.ts +18 -1
- 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.
|
|
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
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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(
|
|
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
|
-
|
|
1068
|
-
|
|
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(
|
|
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
|
|
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(
|
|
1310
|
+
private tryRestoreFromCache(
|
|
1311
|
+
file: IDiscoveredFile,
|
|
1312
|
+
): { usable: boolean } | null {
|
|
1095
1313
|
if (!this.cacheManager?.isValid(file.path)) {
|
|
1096
|
-
return
|
|
1314
|
+
return null;
|
|
1097
1315
|
}
|
|
1098
1316
|
|
|
1099
1317
|
const cached = this.cacheManager.getSymbols(file.path);
|
|
1100
1318
|
if (!cached) {
|
|
1101
|
-
return
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
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
|
+
});
|