c-next 0.2.13 → 0.2.15
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/dist/index.js +106 -50
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/src/cli/ArgParser.ts +2 -6
- package/src/cli/PlatformIOCommand.ts +27 -29
- package/src/cli/__tests__/PlatformIOCommand.test.ts +23 -0
- package/src/transpiler/Transpiler.ts +10 -2
- package/src/transpiler/data/IncludeResolver.ts +11 -1
- package/src/transpiler/data/__tests__/IncludeResolver.test.ts +50 -0
- package/src/transpiler/logic/symbols/c/__tests__/CResolver.integration.test.ts +28 -0
- package/src/transpiler/logic/symbols/c/collectors/VariableCollector.ts +12 -1
- package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +20 -13
- package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +20 -6
- package/src/transpiler/state/CodeGenState.ts +45 -3
- package/src/transpiler/state/__tests__/CodeGenState.test.ts +145 -3
package/package.json
CHANGED
package/src/cli/ArgParser.ts
CHANGED
|
@@ -42,10 +42,8 @@ function configureYargs(args: string[], argv: string[]) {
|
|
|
42
42
|
.scriptName("cnext")
|
|
43
43
|
.usage(
|
|
44
44
|
`Usage:
|
|
45
|
-
cnext <file.cnx>
|
|
45
|
+
cnext <file.cnx> Entry point file (follows includes)
|
|
46
46
|
cnext <file.cnx> -o <output.c> Single file with explicit output
|
|
47
|
-
cnext <files...> -o <dir> Multi-file mode
|
|
48
|
-
cnext <dir> Directory mode (recursive)
|
|
49
47
|
|
|
50
48
|
A safer C for embedded systems development.`,
|
|
51
49
|
)
|
|
@@ -156,10 +154,8 @@ A safer C for embedded systems development.`,
|
|
|
156
154
|
// Config file documentation (shown in help)
|
|
157
155
|
.epilogue(
|
|
158
156
|
`Examples:
|
|
159
|
-
cnext main.cnx
|
|
157
|
+
cnext src/main.cnx # Entry point (follows includes)
|
|
160
158
|
cnext main.cnx -o build/main.c # Explicit output path
|
|
161
|
-
cnext src/*.cnx -o build/ # Multiple files to directory
|
|
162
|
-
cnext src/ # Compile all .cnx files in src/ (recursive)
|
|
163
159
|
|
|
164
160
|
Target platforms: teensy41, cortex-m7, cortex-m4, cortex-m3, cortex-m0+, cortex-m0, avr
|
|
165
161
|
|
|
@@ -46,37 +46,36 @@ class PlatformIOCommand {
|
|
|
46
46
|
// Create cnext_build.py script
|
|
47
47
|
// Issue #833: Run transpilation at import time (before compilation),
|
|
48
48
|
// not as a pre-action on buildprog (which runs after compilation)
|
|
49
|
-
const buildScript = `Import("env")
|
|
49
|
+
const buildScript = String.raw`Import("env")
|
|
50
50
|
import subprocess
|
|
51
51
|
import sys
|
|
52
52
|
from pathlib import Path
|
|
53
53
|
|
|
54
54
|
def transpile_cnext():
|
|
55
|
-
"""Transpile
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if not src_dir.exists():
|
|
55
|
+
"""Transpile from main.cnx entry point — cnext follows includes"""
|
|
56
|
+
entry = Path("src/main.cnx")
|
|
57
|
+
if not entry.exists():
|
|
59
58
|
return
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
60
|
+
print("Transpiling from main.cnx...")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
result = subprocess.run(
|
|
64
|
+
["cnext", str(entry)],
|
|
65
|
+
check=True,
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True
|
|
68
|
+
)
|
|
69
|
+
if result.stdout:
|
|
70
|
+
lines = result.stdout.strip().split("\n")
|
|
71
|
+
for line in lines:
|
|
72
|
+
if line.startswith(("Compiled", "Collected", "Generated")):
|
|
73
|
+
print(f" {line}")
|
|
74
|
+
print(" ✓ Transpilation complete")
|
|
75
|
+
except subprocess.CalledProcessError as e:
|
|
76
|
+
print(f" ✗ Transpilation failed")
|
|
77
|
+
print(e.stderr)
|
|
78
|
+
sys.exit(1)
|
|
80
79
|
|
|
81
80
|
# Run transpilation at import time (before compilation starts)
|
|
82
81
|
transpile_cnext()
|
|
@@ -119,13 +118,12 @@ transpile_cnext()
|
|
|
119
118
|
console.log("✓ PlatformIO integration configured!");
|
|
120
119
|
console.log("");
|
|
121
120
|
console.log("Next steps:");
|
|
122
|
-
console.log(
|
|
123
|
-
|
|
124
|
-
);
|
|
125
|
-
console.log(" 2. Run: pio run");
|
|
121
|
+
console.log(" 1. Create src/main.cnx as your entry point");
|
|
122
|
+
console.log(" 2. Use #include to pull in other .cnx files");
|
|
123
|
+
console.log(" 3. Run: pio run");
|
|
126
124
|
console.log("");
|
|
127
125
|
console.log(
|
|
128
|
-
"The transpiler will
|
|
126
|
+
"The transpiler will follow includes from main.cnx automatically.",
|
|
129
127
|
);
|
|
130
128
|
console.log("Commit both .cnx and generated .c files to version control.");
|
|
131
129
|
}
|
|
@@ -68,6 +68,29 @@ describe("PlatformIOCommand", () => {
|
|
|
68
68
|
expect(scriptCall?.[1]).toContain("transpile_cnext");
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
it("generates script that uses entry point, not per-file loop", () => {
|
|
72
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
73
|
+
vi.mocked(fs.readFileSync).mockReturnValue("[env:esp32]\n");
|
|
74
|
+
|
|
75
|
+
PlatformIOCommand.install();
|
|
76
|
+
|
|
77
|
+
const writeCalls = vi.mocked(fs.writeFileSync).mock.calls;
|
|
78
|
+
const scriptCall = writeCalls.find((call) =>
|
|
79
|
+
(call[0] as string).includes("cnext_build.py"),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(scriptCall).toBeDefined();
|
|
83
|
+
const scriptContent = scriptCall?.[1] as string;
|
|
84
|
+
|
|
85
|
+
// Should use entry point approach
|
|
86
|
+
expect(scriptContent).toContain('Path("src/main.cnx")');
|
|
87
|
+
expect(scriptContent).toContain('["cnext", str(entry)]');
|
|
88
|
+
|
|
89
|
+
// Should NOT loop over individual .cnx files
|
|
90
|
+
expect(scriptContent).not.toContain("for cnx_file");
|
|
91
|
+
expect(scriptContent).not.toContain("rglob");
|
|
92
|
+
});
|
|
93
|
+
|
|
71
94
|
it("generates script that runs at import time, not as buildprog pre-action (issue #833)", () => {
|
|
72
95
|
// Issue #833: buildprog fires AFTER compilation, so transpile runs too late
|
|
73
96
|
// The script should run transpilation at import time (before compilation)
|
|
@@ -491,7 +491,11 @@ class Transpiler {
|
|
|
491
491
|
);
|
|
492
492
|
|
|
493
493
|
// Resolve includes from source content
|
|
494
|
-
const resolver = new IncludeResolver(
|
|
494
|
+
const resolver = new IncludeResolver(
|
|
495
|
+
searchPaths,
|
|
496
|
+
this.fs,
|
|
497
|
+
this.cppDetected,
|
|
498
|
+
);
|
|
495
499
|
const resolved = resolver.resolve(source, sourcePath);
|
|
496
500
|
this.warnings.push(...resolved.warnings);
|
|
497
501
|
|
|
@@ -864,7 +868,11 @@ class Transpiler {
|
|
|
864
868
|
);
|
|
865
869
|
|
|
866
870
|
// Resolve includes
|
|
867
|
-
const resolver = new IncludeResolver(
|
|
871
|
+
const resolver = new IncludeResolver(
|
|
872
|
+
searchPaths,
|
|
873
|
+
this.fs,
|
|
874
|
+
this.cppDetected,
|
|
875
|
+
);
|
|
868
876
|
const resolved = resolver.resolve(content, cnxFile.path);
|
|
869
877
|
|
|
870
878
|
this._collectHeaders(resolved, cnextBaseNames, headerSet);
|
|
@@ -60,12 +60,21 @@ class IncludeResolver {
|
|
|
60
60
|
|
|
61
61
|
private readonly resolvedPaths: Set<string> = new Set();
|
|
62
62
|
private readonly fs: IFileSystem;
|
|
63
|
+
private readonly cppMode: boolean;
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @param cppMode Controls .h vs .hpp extension for .cnx include directives.
|
|
67
|
+
* Note: In the Transpiler, cppDetected may change after IncludeResolver runs
|
|
68
|
+
* (e.g., when a .hpp header is discovered during Stage 2). HeaderGeneratorUtils
|
|
69
|
+
* uses stem-based dedup to handle any resulting .h/.hpp mismatch.
|
|
70
|
+
*/
|
|
64
71
|
constructor(
|
|
65
72
|
private readonly searchPaths: string[],
|
|
66
73
|
fs: IFileSystem = defaultFs,
|
|
74
|
+
cppMode: boolean = false,
|
|
67
75
|
) {
|
|
68
76
|
this.fs = fs;
|
|
77
|
+
this.cppMode = cppMode;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/**
|
|
@@ -164,7 +173,8 @@ class IncludeResolver {
|
|
|
164
173
|
// Issue #854: Track header directive for cnext includes so their types
|
|
165
174
|
// can be mapped by ExternalTypeHeaderBuilder, preventing duplicate
|
|
166
175
|
// forward declarations (MISRA Rule 5.6)
|
|
167
|
-
const
|
|
176
|
+
const ext = this.cppMode ? ".hpp" : ".h";
|
|
177
|
+
const headerPath = includeInfo.path.replace(/\.cnx$|\.cnext$/, ext);
|
|
168
178
|
const directive = includeInfo.isLocal
|
|
169
179
|
? `#include "${headerPath}"`
|
|
170
180
|
: `#include <${headerPath}>`;
|
|
@@ -315,6 +315,56 @@ describe("IncludeResolver", () => {
|
|
|
315
315
|
});
|
|
316
316
|
});
|
|
317
317
|
|
|
318
|
+
// ========================================================================
|
|
319
|
+
// C++ Mode (cppMode) — Issue: .hpp vs .h in headerIncludeDirectives
|
|
320
|
+
// ========================================================================
|
|
321
|
+
|
|
322
|
+
describe("cppMode header directive extension", () => {
|
|
323
|
+
it("should use .h extension for cnx includes in C mode (default)", () => {
|
|
324
|
+
const resolver = new IncludeResolver([includeDir]);
|
|
325
|
+
const content = '#include "shared.cnx"';
|
|
326
|
+
|
|
327
|
+
const result = resolver.resolve(content);
|
|
328
|
+
|
|
329
|
+
const directives = [...result.headerIncludeDirectives.values()];
|
|
330
|
+
expect(directives).toHaveLength(1);
|
|
331
|
+
expect(directives[0]).toBe('#include "shared.h"');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should use .hpp extension for cnx includes in C++ mode", () => {
|
|
335
|
+
const resolver = new IncludeResolver([includeDir], undefined, true);
|
|
336
|
+
const content = '#include "shared.cnx"';
|
|
337
|
+
|
|
338
|
+
const result = resolver.resolve(content);
|
|
339
|
+
|
|
340
|
+
const directives = [...result.headerIncludeDirectives.values()];
|
|
341
|
+
expect(directives).toHaveLength(1);
|
|
342
|
+
expect(directives[0]).toBe('#include "shared.hpp"');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should use .hpp for angle-bracket cnx includes in C++ mode", () => {
|
|
346
|
+
const resolver = new IncludeResolver([includeDir], undefined, true);
|
|
347
|
+
const content = "#include <shared.cnx>";
|
|
348
|
+
|
|
349
|
+
const result = resolver.resolve(content);
|
|
350
|
+
|
|
351
|
+
const directives = [...result.headerIncludeDirectives.values()];
|
|
352
|
+
expect(directives).toHaveLength(1);
|
|
353
|
+
expect(directives[0]).toBe("#include <shared.hpp>");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("should not affect C/C++ header directives (only cnx includes)", () => {
|
|
357
|
+
const resolver = new IncludeResolver([includeDir], undefined, true);
|
|
358
|
+
const content = '#include "types.h"';
|
|
359
|
+
|
|
360
|
+
const result = resolver.resolve(content);
|
|
361
|
+
|
|
362
|
+
const directives = [...result.headerIncludeDirectives.values()];
|
|
363
|
+
expect(directives).toHaveLength(1);
|
|
364
|
+
expect(directives[0]).toBe('#include "types.h"');
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
318
368
|
// ========================================================================
|
|
319
369
|
// Edge Cases
|
|
320
370
|
// ========================================================================
|
|
@@ -182,6 +182,34 @@ describe("CResolver - Variable Declarations", () => {
|
|
|
182
182
|
}
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
|
+
|
|
186
|
+
it("detects pointer type in variable declaration (Issue #978)", () => {
|
|
187
|
+
const tree = TestHelpers.parseC(
|
|
188
|
+
`typedef struct { int x; } point_t;\nextern const point_t *origin;`,
|
|
189
|
+
);
|
|
190
|
+
const result = CResolver.resolve(tree!, "test.h");
|
|
191
|
+
|
|
192
|
+
const varSymbol = result.symbols.find((s) => s.name === "origin");
|
|
193
|
+
expect(varSymbol).toBeDefined();
|
|
194
|
+
expect(varSymbol?.kind).toBe("variable");
|
|
195
|
+
if (varSymbol?.kind === "variable") {
|
|
196
|
+
expect(varSymbol.type).toBe("point_t*");
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("does not add pointer suffix for non-pointer variables (Issue #978)", () => {
|
|
201
|
+
const tree = TestHelpers.parseC(
|
|
202
|
+
`typedef struct { int x; } point_t;\nextern const point_t origin;`,
|
|
203
|
+
);
|
|
204
|
+
const result = CResolver.resolve(tree!, "test.h");
|
|
205
|
+
|
|
206
|
+
const varSymbol = result.symbols.find((s) => s.name === "origin");
|
|
207
|
+
expect(varSymbol).toBeDefined();
|
|
208
|
+
expect(varSymbol?.kind).toBe("variable");
|
|
209
|
+
if (varSymbol?.kind === "variable") {
|
|
210
|
+
expect(varSymbol.type).toBe("point_t");
|
|
211
|
+
}
|
|
212
|
+
});
|
|
185
213
|
});
|
|
186
214
|
|
|
187
215
|
describe("CResolver - Typedefs", () => {
|
|
@@ -32,6 +32,13 @@ class VariableCollector {
|
|
|
32
32
|
? DeclaratorUtils.extractArrayDimensions(declarator)
|
|
33
33
|
: [];
|
|
34
34
|
|
|
35
|
+
// Issue #978: Detect pointer variables (e.g., `font_t *ptr`).
|
|
36
|
+
// C grammar puts `*` in the declarator, not the type specifier.
|
|
37
|
+
// Same pattern as FunctionCollector._resolveReturnType().
|
|
38
|
+
const hasPointer =
|
|
39
|
+
declarator?.pointer?.() !== null && declarator?.pointer?.() !== undefined;
|
|
40
|
+
const resolvedType = hasPointer ? `${baseType}*` : baseType;
|
|
41
|
+
|
|
35
42
|
return {
|
|
36
43
|
kind: "variable",
|
|
37
44
|
name,
|
|
@@ -39,7 +46,7 @@ class VariableCollector {
|
|
|
39
46
|
sourceLine: line,
|
|
40
47
|
sourceLanguage: ESourceLanguage.C,
|
|
41
48
|
isExported: !isExtern,
|
|
42
|
-
type:
|
|
49
|
+
type: resolvedType,
|
|
43
50
|
isArray: arrayDimensions.length > 0,
|
|
44
51
|
arrayDimensions: arrayDimensions.length > 0 ? arrayDimensions : undefined,
|
|
45
52
|
isExtern,
|
|
@@ -50,6 +57,10 @@ class VariableCollector {
|
|
|
50
57
|
* Collect a variable from declaration specifiers (when identifier appears as typedefName).
|
|
51
58
|
* This handles the C grammar ambiguity where variable names can be parsed as typedef names.
|
|
52
59
|
*
|
|
60
|
+
* Note: No pointer detection here — this path handles declarations without an
|
|
61
|
+
* initDeclaratorList. Pointer declarations (e.g., `font_t *ptr`) always produce
|
|
62
|
+
* an initDeclaratorList (the `*` creates a declarator), so they go through collect().
|
|
63
|
+
*
|
|
53
64
|
* @param name Variable name
|
|
54
65
|
* @param baseType Variable type
|
|
55
66
|
* @param sourceFile Source file path
|
|
@@ -289,22 +289,29 @@ class HeaderGeneratorUtils {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
// External type header includes (skip duplicates of user includes)
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
292
|
+
// Dedup by basename stem to handle:
|
|
293
|
+
// - Different path styles (e.g., <AppConfig.hpp> vs "../AppConfig.hpp")
|
|
294
|
+
// - Extension mismatch from timing (.h from IncludeResolver before cppDetected,
|
|
295
|
+
// .hpp from IncludeExtractor after cppDetected)
|
|
295
296
|
const userIncludeSet = new Set(options.userIncludes ?? []);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
297
|
+
const extractStem = (inc: string): string => {
|
|
298
|
+
const match = /["<]([^">]+)[">]/.exec(inc);
|
|
299
|
+
if (!match) return inc;
|
|
300
|
+
return match[1].replace(/^.*\//, "").replace(/\.(?:h|hpp)$/, "");
|
|
301
|
+
};
|
|
302
|
+
const userIncludeStems = new Set(
|
|
303
|
+
(options.userIncludes ?? []).map(extractStem),
|
|
304
|
+
);
|
|
304
305
|
for (const directive of headersToInclude) {
|
|
305
|
-
if (
|
|
306
|
-
|
|
306
|
+
if (userIncludeSet.has(directive)) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
// Check if a user include already covers the same file
|
|
310
|
+
const stem = extractStem(directive);
|
|
311
|
+
if (stem && userIncludeStems.has(stem)) {
|
|
312
|
+
continue;
|
|
307
313
|
}
|
|
314
|
+
lines.push(directive);
|
|
308
315
|
}
|
|
309
316
|
|
|
310
317
|
// Add blank line if any includes were added
|
|
@@ -571,17 +571,31 @@ describe("HeaderGeneratorUtils", () => {
|
|
|
571
571
|
expect(lines).toContain('#include "types.hpp"');
|
|
572
572
|
});
|
|
573
573
|
|
|
574
|
-
it("deduplicates headersToInclude against userIncludes
|
|
574
|
+
it("deduplicates headersToInclude against userIncludes by basename in C++ mode", () => {
|
|
575
575
|
const result = HeaderGeneratorUtils.generateIncludes(
|
|
576
576
|
{
|
|
577
|
-
userIncludes: [
|
|
577
|
+
userIncludes: ["#include <AppConfig.hpp>"],
|
|
578
|
+
cppMode: true,
|
|
579
|
+
},
|
|
580
|
+
new Set(['#include "../AppConfig.hpp"']),
|
|
581
|
+
);
|
|
582
|
+
// Should NOT have duplicate - different path styles for same file should dedup
|
|
583
|
+
const configIncludes = result.filter((l) => l.includes("AppConfig"));
|
|
584
|
+
expect(configIncludes).toEqual(["#include <AppConfig.hpp>"]);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("deduplicates .h vs .hpp extension mismatch from timing difference", () => {
|
|
588
|
+
// IncludeExtractor produces .hpp (after cppDetected), IncludeResolver
|
|
589
|
+
// produces .h (before cppDetected) — stem-based dedup handles this
|
|
590
|
+
const result = HeaderGeneratorUtils.generateIncludes(
|
|
591
|
+
{
|
|
592
|
+
userIncludes: ["#include <Display/AppData.hpp>"],
|
|
578
593
|
cppMode: true,
|
|
579
594
|
},
|
|
580
|
-
new Set([
|
|
595
|
+
new Set(["#include <Display/AppData.h>"]),
|
|
581
596
|
);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
expect(typeIncludes).toEqual(['#include "types.hpp"']);
|
|
597
|
+
const appDataIncludes = result.filter((l) => l.includes("AppData"));
|
|
598
|
+
expect(appDataIncludes).toEqual(["#include <Display/AppData.hpp>"]);
|
|
585
599
|
});
|
|
586
600
|
});
|
|
587
601
|
|
|
@@ -554,13 +554,31 @@ export default class CodeGenState {
|
|
|
554
554
|
}
|
|
555
555
|
|
|
556
556
|
// ADR-055 Phase 7: Fall back to SymbolTable for cross-file C-Next variables only.
|
|
557
|
-
// C/C++ header symbols don't have complete type info (e.g., isArray),
|
|
558
|
-
// so we only use C-Next TSymbols from SymbolTable.
|
|
559
557
|
const symbol = this.symbolTable.getTSymbol(name);
|
|
560
558
|
if (symbol?.kind === "variable" && symbol.type) {
|
|
561
559
|
return this.convertTSymbolToTypeInfo(symbol);
|
|
562
560
|
}
|
|
563
561
|
|
|
562
|
+
// Issue #978: Fall back to C symbols for external struct globals from .h headers.
|
|
563
|
+
// Only return type info for struct-typed variables — returning info for all
|
|
564
|
+
// C types would cause regressions (e.g., array indexing misread as bit extraction).
|
|
565
|
+
const cSymbol = this.symbolTable.getCSymbol(name);
|
|
566
|
+
if (cSymbol?.kind === "variable" && cSymbol.type) {
|
|
567
|
+
const baseType = CodeGenState.stripTrailingPointers(cSymbol.type);
|
|
568
|
+
if (
|
|
569
|
+
this.symbolTable.isTypedefStructType(baseType) ||
|
|
570
|
+
this.symbolTable.getStructFields(baseType)
|
|
571
|
+
) {
|
|
572
|
+
return {
|
|
573
|
+
baseType,
|
|
574
|
+
bitWidth: 0,
|
|
575
|
+
isArray: cSymbol.isArray || false,
|
|
576
|
+
isConst: cSymbol.isConst || false,
|
|
577
|
+
isPointer: cSymbol.type.endsWith("*"),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
564
582
|
return undefined;
|
|
565
583
|
}
|
|
566
584
|
|
|
@@ -581,7 +599,19 @@ export default class CodeGenState {
|
|
|
581
599
|
return true;
|
|
582
600
|
}
|
|
583
601
|
const symbol = this.symbolTable.getTSymbol(name);
|
|
584
|
-
|
|
602
|
+
if (symbol?.kind === "variable" && symbol.type !== undefined) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
// Issue #978: Check C symbols for external struct globals only
|
|
606
|
+
const cSymbol = this.symbolTable.getCSymbol(name);
|
|
607
|
+
if (cSymbol?.kind === "variable" && cSymbol.type) {
|
|
608
|
+
const baseType = CodeGenState.stripTrailingPointers(cSymbol.type);
|
|
609
|
+
return (
|
|
610
|
+
this.symbolTable.isTypedefStructType(baseType) ||
|
|
611
|
+
!!this.symbolTable.getStructFields(baseType)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return false;
|
|
585
615
|
}
|
|
586
616
|
|
|
587
617
|
/**
|
|
@@ -611,6 +641,18 @@ export default class CodeGenState {
|
|
|
611
641
|
* Convert a TSymbol IVariableSymbol to TTypeInfo for unified type lookups.
|
|
612
642
|
* ADR-055 Phase 7: Works with typed TSymbol instead of ISymbol.
|
|
613
643
|
*/
|
|
644
|
+
/**
|
|
645
|
+
* Strip trailing pointer stars from a C type string (e.g., "font_t*" → "font_t").
|
|
646
|
+
* Uses string operations instead of regex to avoid SonarCloud ReDoS flag (S5852).
|
|
647
|
+
*/
|
|
648
|
+
private static stripTrailingPointers(type: string): string {
|
|
649
|
+
let end = type.length;
|
|
650
|
+
while (end > 0 && type[end - 1] === "*") {
|
|
651
|
+
end--;
|
|
652
|
+
}
|
|
653
|
+
return type.slice(0, end).trim();
|
|
654
|
+
}
|
|
655
|
+
|
|
614
656
|
private static convertTSymbolToTypeInfo(
|
|
615
657
|
symbol: import("../types/symbols/IVariableSymbol").default,
|
|
616
658
|
): TTypeInfo {
|
|
@@ -105,6 +105,7 @@ function createMockSymbols(
|
|
|
105
105
|
describe("CodeGenState", () => {
|
|
106
106
|
beforeEach(() => {
|
|
107
107
|
CodeGenState.reset();
|
|
108
|
+
CodeGenState.symbolTable.clear();
|
|
108
109
|
});
|
|
109
110
|
|
|
110
111
|
describe("reset()", () => {
|
|
@@ -435,8 +436,8 @@ describe("CodeGenState", () => {
|
|
|
435
436
|
expect(result?.arrayDimensions).toEqual([10]);
|
|
436
437
|
});
|
|
437
438
|
|
|
438
|
-
it("getVariableTypeInfo does not use C header symbols", () => {
|
|
439
|
-
// Add a C header variable (should NOT be used)
|
|
439
|
+
it("getVariableTypeInfo does not use C header symbols for primitive types", () => {
|
|
440
|
+
// Add a C header variable with primitive type (should NOT be used)
|
|
440
441
|
CodeGenState.symbolTable.addCSymbol(
|
|
441
442
|
createCVariableSymbol({
|
|
442
443
|
name: "cHeaderVar",
|
|
@@ -447,6 +448,110 @@ describe("CodeGenState", () => {
|
|
|
447
448
|
expect(CodeGenState.getVariableTypeInfo("cHeaderVar")).toBeUndefined();
|
|
448
449
|
});
|
|
449
450
|
|
|
451
|
+
it("getVariableTypeInfo returns type info for C header struct variables (Issue #978)", () => {
|
|
452
|
+
// Register font_t as a typedef struct type
|
|
453
|
+
CodeGenState.symbolTable.markTypedefStructType("font_t", "fake_lib.h");
|
|
454
|
+
|
|
455
|
+
// Add a C header variable with struct type
|
|
456
|
+
CodeGenState.symbolTable.addCSymbol(
|
|
457
|
+
createCVariableSymbol({
|
|
458
|
+
name: "big_font",
|
|
459
|
+
type: "font_t",
|
|
460
|
+
isConst: true,
|
|
461
|
+
}),
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const result = CodeGenState.getVariableTypeInfo("big_font");
|
|
465
|
+
expect(result).toBeDefined();
|
|
466
|
+
expect(result?.baseType).toBe("font_t");
|
|
467
|
+
expect(result?.isConst).toBe(true);
|
|
468
|
+
expect(result?.bitWidth).toBe(0);
|
|
469
|
+
expect(result?.isPointer).toBe(false);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("getVariableTypeInfo returns type info for C struct via getStructFields path (Issue #978)", () => {
|
|
473
|
+
// Register struct fields directly (non-typedef struct, e.g., `struct point`)
|
|
474
|
+
CodeGenState.symbolTable.addStructField("point", "x", "int32_t");
|
|
475
|
+
CodeGenState.symbolTable.addStructField("point", "y", "int32_t");
|
|
476
|
+
|
|
477
|
+
CodeGenState.symbolTable.addCSymbol(
|
|
478
|
+
createCVariableSymbol({
|
|
479
|
+
name: "origin",
|
|
480
|
+
type: "point",
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
const result = CodeGenState.getVariableTypeInfo("origin");
|
|
485
|
+
expect(result).toBeDefined();
|
|
486
|
+
expect(result?.baseType).toBe("point");
|
|
487
|
+
expect(result?.bitWidth).toBe(0);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("getVariableTypeInfo detects pointer type from C symbol (Issue #978)", () => {
|
|
491
|
+
// Register font_t as a struct
|
|
492
|
+
CodeGenState.symbolTable.markTypedefStructType("font_t", "lib.h");
|
|
493
|
+
|
|
494
|
+
// Pointer variable: type includes * (set by VariableCollector)
|
|
495
|
+
CodeGenState.symbolTable.addCSymbol(
|
|
496
|
+
createCVariableSymbol({
|
|
497
|
+
name: "font_ptr",
|
|
498
|
+
type: "font_t*",
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const result = CodeGenState.getVariableTypeInfo("font_ptr");
|
|
503
|
+
expect(result).toBeDefined();
|
|
504
|
+
expect(result?.baseType).toBe("font_t");
|
|
505
|
+
expect(result?.isPointer).toBe(true);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("getVariableTypeInfo ignores C array variables with primitive types", () => {
|
|
509
|
+
CodeGenState.symbolTable.addCSymbol(
|
|
510
|
+
createCVariableSymbol({
|
|
511
|
+
name: "lookup_table",
|
|
512
|
+
type: "uint8_t",
|
|
513
|
+
isArray: true,
|
|
514
|
+
arrayDimensions: [16],
|
|
515
|
+
}),
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
expect(CodeGenState.getVariableTypeInfo("lookup_table")).toBeUndefined();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("getVariableTypeInfo ignores C volatile register variables", () => {
|
|
522
|
+
CodeGenState.symbolTable.addCSymbol(
|
|
523
|
+
createCVariableSymbol({
|
|
524
|
+
name: "status_reg",
|
|
525
|
+
type: "uint32_t",
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
expect(CodeGenState.getVariableTypeInfo("status_reg")).toBeUndefined();
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("getVariableTypeInfo prefers TSymbol over CSymbol with same name (Issue #978)", () => {
|
|
533
|
+
// Both C-Next and C symbols exist with same name
|
|
534
|
+
CodeGenState.symbolTable.markTypedefStructType("config_t", "config.h");
|
|
535
|
+
|
|
536
|
+
CodeGenState.symbolTable.addTSymbol(
|
|
537
|
+
createCNextVariableSymbol({
|
|
538
|
+
name: "config",
|
|
539
|
+
type: TTypeUtils.createPrimitive("u32"),
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
CodeGenState.symbolTable.addCSymbol(
|
|
544
|
+
createCVariableSymbol({
|
|
545
|
+
name: "config",
|
|
546
|
+
type: "config_t",
|
|
547
|
+
}),
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
// TSymbol should win (checked first in priority order)
|
|
551
|
+
const result = CodeGenState.getVariableTypeInfo("config");
|
|
552
|
+
expect(result?.baseType).toBe("u32");
|
|
553
|
+
});
|
|
554
|
+
|
|
450
555
|
it("getVariableTypeInfo prefers local registry over SymbolTable", () => {
|
|
451
556
|
// Add both local and SymbolTable version
|
|
452
557
|
const localInfo: TTypeInfo = {
|
|
@@ -496,7 +601,7 @@ describe("CodeGenState", () => {
|
|
|
496
601
|
expect(CodeGenState.hasVariableTypeInfo("unknownVar")).toBe(false);
|
|
497
602
|
});
|
|
498
603
|
|
|
499
|
-
it("hasVariableTypeInfo returns false for C header variable", () => {
|
|
604
|
+
it("hasVariableTypeInfo returns false for C header primitive variable", () => {
|
|
500
605
|
CodeGenState.symbolTable.addCSymbol(
|
|
501
606
|
createCVariableSymbol({
|
|
502
607
|
name: "cVar",
|
|
@@ -507,6 +612,43 @@ describe("CodeGenState", () => {
|
|
|
507
612
|
expect(CodeGenState.hasVariableTypeInfo("cVar")).toBe(false);
|
|
508
613
|
});
|
|
509
614
|
|
|
615
|
+
it("hasVariableTypeInfo returns true for C header struct variable (Issue #978)", () => {
|
|
616
|
+
CodeGenState.symbolTable.markTypedefStructType("widget_t", "widget.h");
|
|
617
|
+
CodeGenState.symbolTable.addCSymbol(
|
|
618
|
+
createCVariableSymbol({
|
|
619
|
+
name: "my_widget",
|
|
620
|
+
type: "widget_t",
|
|
621
|
+
}),
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
expect(CodeGenState.hasVariableTypeInfo("my_widget")).toBe(true);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("hasVariableTypeInfo returns true for C struct via getStructFields path (Issue #978)", () => {
|
|
628
|
+
CodeGenState.symbolTable.addStructField("vec2", "x", "float");
|
|
629
|
+
CodeGenState.symbolTable.addStructField("vec2", "y", "float");
|
|
630
|
+
|
|
631
|
+
CodeGenState.symbolTable.addCSymbol(
|
|
632
|
+
createCVariableSymbol({
|
|
633
|
+
name: "position",
|
|
634
|
+
type: "vec2",
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
expect(CodeGenState.hasVariableTypeInfo("position")).toBe(true);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it("hasVariableTypeInfo returns false for C pointer to non-struct type", () => {
|
|
642
|
+
CodeGenState.symbolTable.addCSymbol(
|
|
643
|
+
createCVariableSymbol({
|
|
644
|
+
name: "data_ptr",
|
|
645
|
+
type: "uint8_t*",
|
|
646
|
+
}),
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
expect(CodeGenState.hasVariableTypeInfo("data_ptr")).toBe(false);
|
|
650
|
+
});
|
|
651
|
+
|
|
510
652
|
it("setVariableTypeInfo and deleteVariableTypeInfo work correctly", () => {
|
|
511
653
|
const typeInfo: TTypeInfo = {
|
|
512
654
|
baseType: "f32",
|