c-next 0.2.16 → 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 (118) hide show
  1. package/README.md +18 -2
  2. package/dist/index.js +8897 -6260
  3. package/dist/index.js.map +4 -4
  4. package/grammar/CNext.g4 +12 -0
  5. package/package.json +4 -2
  6. package/src/transpiler/Transpiler.ts +376 -48
  7. package/src/transpiler/__tests__/DualCodePaths.test.ts +1 -1
  8. package/src/transpiler/__tests__/compileCommandsDiscovery.integration.test.ts +94 -0
  9. package/src/transpiler/__tests__/externalSymbolRecovery.integration.test.ts +215 -0
  10. package/src/transpiler/logic/__tests__/detectAssemblySyntax.test.ts +116 -0
  11. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +65 -10
  12. package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +186 -14
  13. package/src/transpiler/logic/analysis/MixedTypeCategoryAnalyzer.ts +408 -0
  14. package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +16 -97
  15. package/src/transpiler/logic/analysis/ReturnPathAnalyzer.ts +171 -0
  16. package/src/transpiler/logic/analysis/SignedShiftAnalyzer.ts +124 -12
  17. package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +200 -0
  18. package/src/transpiler/logic/analysis/__tests__/InitializationAnalyzer.test.ts +386 -1
  19. package/src/transpiler/logic/analysis/__tests__/MixedTypeCategoryAnalyzer.test.ts +346 -0
  20. package/src/transpiler/logic/analysis/__tests__/ReturnPathAnalyzer.test.ts +232 -0
  21. package/src/transpiler/logic/analysis/__tests__/SignedShiftAnalyzer.test.ts +211 -0
  22. package/src/transpiler/logic/analysis/runAnalyzers.ts +23 -2
  23. package/src/transpiler/logic/analysis/types/IMixedTypeCategoryError.ts +17 -0
  24. package/src/transpiler/logic/analysis/types/IReturnPathError.ts +12 -0
  25. package/src/transpiler/logic/detectAssemblySyntax.ts +80 -0
  26. package/src/transpiler/logic/parser/grammar/CNext.interp +4 -1
  27. package/src/transpiler/logic/parser/grammar/CNext.tokens +169 -167
  28. package/src/transpiler/logic/parser/grammar/CNextLexer.interp +4 -1
  29. package/src/transpiler/logic/parser/grammar/CNextLexer.tokens +169 -167
  30. package/src/transpiler/logic/parser/grammar/CNextLexer.ts +545 -541
  31. package/src/transpiler/logic/parser/grammar/CNextListener.ts +11 -0
  32. package/src/transpiler/logic/parser/grammar/CNextParser.ts +1318 -1178
  33. package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +7 -0
  34. package/src/transpiler/logic/preprocessor/CompileCommandsReader.ts +332 -0
  35. package/src/transpiler/logic/preprocessor/ExternalDeclarationOracle.ts +202 -0
  36. package/src/transpiler/logic/preprocessor/Preprocessor.ts +24 -6
  37. package/src/transpiler/logic/preprocessor/ToolchainDetector.ts +42 -1
  38. package/src/transpiler/logic/preprocessor/__tests__/CompileCommandsReader.test.ts +216 -0
  39. package/src/transpiler/logic/preprocessor/__tests__/ExternalDeclarationOracle.test.ts +153 -0
  40. package/src/transpiler/logic/preprocessor/__tests__/Preprocessor.test.ts +43 -18
  41. package/src/transpiler/logic/preprocessor/__tests__/ToolchainDetector.crossCompiler.test.ts +75 -0
  42. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/dependent.h +7 -0
  43. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/predecessor.h +5 -0
  44. package/src/transpiler/logic/preprocessor/types/ICompileCommandsResult.ts +14 -0
  45. package/src/transpiler/logic/preprocessor/types/IPreprocessOptions.ts +17 -0
  46. package/src/transpiler/logic/symbols/SymbolTable.ts +62 -2
  47. package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +55 -4
  48. package/src/transpiler/logic/symbols/cnext/collectors/VariableCollector.ts +15 -2
  49. package/src/transpiler/logic/symbols/cnext/utils/TypeUtils.ts +64 -50
  50. package/src/transpiler/output/MisraSuppressionUtils.ts +52 -0
  51. package/src/transpiler/output/__tests__/MisraSuppressionUtils.test.ts +67 -0
  52. package/src/transpiler/output/codegen/CodeGenerator.ts +196 -98
  53. package/src/transpiler/output/codegen/TypeResolver.ts +148 -0
  54. package/src/transpiler/output/codegen/TypeValidator.ts +179 -81
  55. package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +165 -17
  56. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +163 -35
  57. package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +2 -2
  58. package/src/transpiler/output/codegen/__tests__/TypeResolver.test.ts +93 -0
  59. package/src/transpiler/output/codegen/__tests__/TypeValidator.alwaysTrueLoop.test.ts +91 -0
  60. package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +97 -14
  61. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +13 -0
  62. package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +1 -1
  63. package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +20 -12
  64. package/src/transpiler/output/codegen/assignment/handlers/ArrayHandlers.ts +424 -22
  65. package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +31 -18
  66. package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +7 -8
  67. package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +6 -8
  68. package/src/transpiler/output/codegen/assignment/handlers/RegisterUtils.ts +12 -10
  69. package/src/transpiler/output/codegen/assignment/handlers/SimpleHandler.ts +3 -7
  70. package/src/transpiler/output/codegen/assignment/handlers/SpecialHandlers.ts +7 -9
  71. package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +12 -10
  72. package/src/transpiler/output/codegen/assignment/handlers/__tests__/ArrayHandlers.test.ts +479 -11
  73. package/src/transpiler/output/codegen/generators/IOrchestrator.ts +3 -0
  74. package/src/transpiler/output/codegen/generators/declarationGenerators/ScopeGenerator.ts +26 -7
  75. package/src/transpiler/output/codegen/generators/declarationGenerators/__tests__/ScopeGenerator.test.ts +86 -0
  76. package/src/transpiler/output/codegen/generators/expressions/BinaryExprGenerator.ts +43 -24
  77. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +76 -58
  78. package/src/transpiler/output/codegen/generators/expressions/ExpressionGenerator.ts +9 -2
  79. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +26 -13
  80. package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +44 -0
  81. package/src/transpiler/output/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts +82 -1
  82. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +129 -1
  83. package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +64 -8
  84. package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +8 -5
  85. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +30 -2
  86. package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +17 -3
  87. package/src/transpiler/output/codegen/helpers/ParameterSignatureBuilder.ts +17 -4
  88. package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +240 -42
  89. package/src/transpiler/output/codegen/helpers/SymbolLookupHelper.ts +0 -21
  90. package/src/transpiler/output/codegen/helpers/TypeGenerationHelper.ts +60 -39
  91. package/src/transpiler/output/codegen/helpers/TypeRegistrationEngine.ts +170 -36
  92. package/src/transpiler/output/codegen/helpers/VariableDeclHelper.ts +37 -39
  93. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +19 -0
  94. package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +117 -0
  95. package/src/transpiler/output/codegen/helpers/__tests__/ParameterSignatureBuilder.test.ts +94 -2
  96. package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +322 -9
  97. package/src/transpiler/output/codegen/helpers/__tests__/SymbolLookupHelper.test.ts +0 -64
  98. package/src/transpiler/output/codegen/helpers/__tests__/TypeRegistrationEngine.test.ts +101 -0
  99. package/src/transpiler/output/codegen/helpers/__tests__/VariableDeclHelper.test.ts +29 -5
  100. package/src/transpiler/output/codegen/subscript/SubscriptClassifier.ts +30 -11
  101. package/src/transpiler/output/codegen/subscript/TSubscriptKind.ts +1 -1
  102. package/src/transpiler/output/codegen/subscript/__tests__/SubscriptClassifier.test.ts +38 -6
  103. package/src/transpiler/output/codegen/types/ICallbackTypeInfo.ts +2 -1
  104. package/src/transpiler/output/codegen/types/IParameterInput.ts +7 -0
  105. package/src/transpiler/output/headers/BaseHeaderGenerator.ts +8 -0
  106. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +140 -24
  107. package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +280 -0
  108. package/src/transpiler/output/headers/generators/IHeaderTypeInput.ts +13 -0
  109. package/src/transpiler/output/headers/generators/generateStructHeader.ts +48 -28
  110. package/src/transpiler/state/CodeGenState.ts +91 -22
  111. package/src/transpiler/state/__tests__/CodeGenState.test.ts +253 -11
  112. package/src/transpiler/types/ICachedFileEntry.ts +7 -0
  113. package/src/utils/LiteralUtils.ts +23 -0
  114. package/src/utils/__tests__/LiteralUtils.test.ts +101 -0
  115. package/src/utils/cache/CacheManager.ts +13 -2
  116. package/src/utils/cache/__tests__/CacheManager.test.ts +18 -1
  117. package/src/utils/constants/TypeConstants.ts +13 -0
  118. package/src/utils/types/IParameterSymbol.ts +1 -0
@@ -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
+ });
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Integration test for Issue #985 external-symbol recovery.
3
+ *
4
+ * Drives the real Transpiler over a fixture that forces the recovery path:
5
+ * widget.h refuses standalone inclusion (its #error fires unless guard.h ran
6
+ * first), its API is emitted by a guard.h macro and decorated with a trailing
7
+ * attribute macro, and it declares a pointer-parameter function plus an opaque
8
+ * handle. This exercises Transpiler._collectExternalDeclarations and its
9
+ * helpers, ExternalDeclarationOracle, and the downstream codegen that adds `&`
10
+ * for a struct passed to a pointer param and `*` for an opaque handle.
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 { mkdtempSync, writeFileSync, rmSync, readFileSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import Transpiler from "../Transpiler";
19
+ import CodeGenState from "../state/CodeGenState";
20
+ import Preprocessor from "../logic/preprocessor/Preprocessor";
21
+
22
+ const GUARD_H = `#define WIDGET_GUARD 1
23
+ #define WIDGET_FEATURE 1
24
+ #define USE_PTHREAD 0
25
+ #define PRIVILEGED
26
+ #define WIDGET_SCALE(x) ((x) + 1)
27
+ #define DECLARE_WIDGET_API \\
28
+ typedef struct _widget_t widget_t; \\
29
+ typedef struct { int mode; } widget_cfg_t; \\
30
+ extern const widget_cfg_t widget_default_cfg; \\
31
+ int widget_install(const widget_cfg_t *config) PRIVILEGED; \\
32
+ void widget_apply(const widget_cfg_t *cfg) PRIVILEGED; \\
33
+ widget_t *widget_create(void) PRIVILEGED;
34
+ `;
35
+
36
+ // A header cnext DISCOVERS (it walks #includes unconditionally) but that cannot
37
+ // preprocess standalone — its <no-such-header> mirrors lvgl's OSAL headers
38
+ // probing <semaphore.h>. Its standalone failure sets anyHeaderPreprocessFailed,
39
+ // which is what triggers the external-declaration recovery pass. It is guarded
40
+ // out of the recovery TU (USE_PTHREAD is 0), so the union still preprocesses.
41
+ const PTHREAD_IMPL_H = `#include <cnext_no_such_header_zzz.h>\n`;
42
+
43
+ const WIDGET_H = `#ifndef WIDGET_GUARD
44
+ #error "include guard.h before widget.h"
45
+ #endif
46
+ #if USE_PTHREAD
47
+ #include "pthread_impl.h"
48
+ #endif
49
+ #if WIDGET_FEATURE
50
+ DECLARE_WIDGET_API
51
+ #endif
52
+ `;
53
+
54
+ const MAIN_CNX = `#include "guard.h"
55
+ #include "widget.h"
56
+
57
+ scope Demo {
58
+ widget_t handle;
59
+
60
+ void build() {
61
+ widget_cfg_t cfg <- {mode: 1};
62
+ i32 err <- global.widget_install(cfg);
63
+ global.widget_apply(widget_default_cfg);
64
+ i32 scaled <- WIDGET_SCALE(4);
65
+ handle <- global.widget_create();
66
+ }
67
+ }
68
+ `;
69
+
70
+ describe("external-symbol recovery (integration)", () => {
71
+ let dir: string;
72
+ const available = new Preprocessor().isAvailable();
73
+
74
+ beforeAll(() => {
75
+ dir = mkdtempSync(join(tmpdir(), "cnext-recovery-"));
76
+ writeFileSync(join(dir, "guard.h"), GUARD_H);
77
+ writeFileSync(join(dir, "widget.h"), WIDGET_H);
78
+ writeFileSync(join(dir, "pthread_impl.h"), PTHREAD_IMPL_H);
79
+ writeFileSync(join(dir, "main.cnx"), MAIN_CNX);
80
+ });
81
+
82
+ afterAll(() => {
83
+ if (dir) rmSync(dir, { recursive: true, force: true });
84
+ });
85
+
86
+ it("recovers full signatures so codegen adds & and pointer handles", async () => {
87
+ if (!available) return; // no toolchain in this env
88
+
89
+ const transpiler = new Transpiler({
90
+ input: join(dir, "main.cnx"),
91
+ includeDirs: [dir],
92
+ outDir: join(dir, "out"),
93
+ cppRequired: true,
94
+ noCache: true,
95
+ });
96
+
97
+ const result = await transpiler.transpile({ kind: "files" });
98
+
99
+ const unresolved = result.errors.find(
100
+ (e) =>
101
+ e.message.includes("widget_install") ||
102
+ e.message.includes("widget_create") ||
103
+ // Function-like macro recovered by name (no declaration to parse).
104
+ e.message.includes("WIDGET_SCALE"),
105
+ );
106
+ expect(unresolved).toBeUndefined();
107
+ expect(result.success).toBe(true);
108
+
109
+ const generated = result.outputFiles.find((f) => f.endsWith("main.cpp"));
110
+ expect(generated).toBeDefined();
111
+ const code = readFileSync(generated!, "utf8");
112
+
113
+ // Local struct arg passed to a pointer param -> address-of.
114
+ expect(code).toMatch(/widget_install\(\s*&/);
115
+ // Extern C global (struct value) passed to a pointer param -> address-of,
116
+ // resolved via the C symbol table fallback (#985).
117
+ expect(code).toMatch(/widget_apply\(\s*&widget_default_cfg/);
118
+ // Opaque handle -> pointer field.
119
+ expect(code).toMatch(/widget_t\s*\*\s*\w*handle/);
120
+ });
121
+
122
+ it("folds a recovered struct into externalStructFields so it stays subject to init-completeness checking", async () => {
123
+ if (!available) return;
124
+
125
+ // widget_cfg_t only becomes known through #985 recovery (its typedef is
126
+ // emitted by the DECLARE_WIDGET_API macro inside the un-standalone header).
127
+ // The external-struct snapshot InitializationAnalyzer consults must include
128
+ // it — otherwise recovered structs escape init checking that cleanly
129
+ // preprocessed structs get. Regression: the snapshot must be taken AFTER the
130
+ // recovery pass, not before it.
131
+ const transpiler = new Transpiler({
132
+ input: join(dir, "main.cnx"),
133
+ includeDirs: [dir],
134
+ outDir: join(dir, "out"),
135
+ cppRequired: true,
136
+ noCache: true,
137
+ });
138
+
139
+ const result = await transpiler.transpile({ kind: "files" });
140
+ expect(result.success).toBe(true);
141
+
142
+ const external = CodeGenState.getExternalStructFields();
143
+ expect(external.has("widget_cfg_t")).toBe(true);
144
+ expect([...(external.get("widget_cfg_t") ?? [])]).toContain("mode");
145
+ });
146
+
147
+ it("re-applies recovery on a warm header cache (Issue #985 regression)", async () => {
148
+ if (!available) return;
149
+
150
+ // Fresh project dir with a project marker so on-disk caching activates
151
+ // (determineProjectRoot needs a marker like cnext.config.json). The first
152
+ // run populates .cnx/ with the DEGRADED header symbols (cached before the
153
+ // recovery pass runs); the second run restores them from cache. The
154
+ // recovery gate must survive caching so warm builds re-apply the fix.
155
+ const cacheDir = mkdtempSync(join(tmpdir(), "cnext-recovery-cache-"));
156
+ try {
157
+ writeFileSync(join(cacheDir, "cnext.config.json"), "{}\n");
158
+ writeFileSync(join(cacheDir, "guard.h"), GUARD_H);
159
+ writeFileSync(join(cacheDir, "widget.h"), WIDGET_H);
160
+ writeFileSync(join(cacheDir, "pthread_impl.h"), PTHREAD_IMPL_H);
161
+ writeFileSync(join(cacheDir, "main.cnx"), MAIN_CNX);
162
+
163
+ // Caching ON (noCache defaults to false).
164
+ const run = async () =>
165
+ new Transpiler({
166
+ input: join(cacheDir, "main.cnx"),
167
+ includeDirs: [cacheDir],
168
+ outDir: join(cacheDir, "out"),
169
+ cppRequired: true,
170
+ }).transpile({ kind: "files" });
171
+
172
+ // Cold cache: populates .cnx/ with the degraded header symbols.
173
+ const cold = await run();
174
+ expect(cold.success).toBe(true);
175
+
176
+ // Warm cache: headers unchanged and restored from cache. The recovery
177
+ // pass must still fire and produce the same corrected codegen — before
178
+ // the fix, the warm build regenerated the by-value / non-compiling output.
179
+ const warm = await run();
180
+ expect(warm.success).toBe(true);
181
+
182
+ const generated = warm.outputFiles.find((f) => f.endsWith("main.cpp"));
183
+ expect(generated).toBeDefined();
184
+ const code = readFileSync(generated!, "utf8");
185
+ expect(code).toMatch(/widget_install\(\s*&/);
186
+ expect(code).toMatch(/widget_apply\(\s*&widget_default_cfg/);
187
+ expect(code).toMatch(/widget_t\s*\*\s*\w*handle/);
188
+ } finally {
189
+ rmSync(cacheDir, { recursive: true, force: true });
190
+ }
191
+ });
192
+
193
+ it("does nothing when no header failed to preprocess (clean project)", async () => {
194
+ if (!available) return;
195
+
196
+ // A .cnx with no C includes never trips anyHeaderPreprocessFailed, so the
197
+ // recovery pass is a no-op and transpilation still succeeds.
198
+ const cleanDir = mkdtempSync(join(tmpdir(), "cnext-clean-"));
199
+ try {
200
+ writeFileSync(
201
+ join(cleanDir, "main.cnx"),
202
+ `scope Demo {\n void run() {\n i32 x <- 1;\n }\n}\n`,
203
+ );
204
+ const transpiler = new Transpiler({
205
+ input: join(cleanDir, "main.cnx"),
206
+ outDir: join(cleanDir, "out"),
207
+ noCache: true,
208
+ });
209
+ const result = await transpiler.transpile({ kind: "files" });
210
+ expect(result.success).toBe(true);
211
+ } finally {
212
+ rmSync(cleanDir, { recursive: true, force: true });
213
+ }
214
+ });
215
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Unit tests for detectAssemblySyntax
3
+ *
4
+ * Some framework headers (e.g. xtensa `coreasm.h`, pulled in transitively by
5
+ * FreeRTOS port headers) are GNU-assembler sources, not C: they `#define
6
+ * _ASMLANGUAGE` and contain `.macro` bodies whose instruction mnemonics (e.g.
7
+ * `loop`) were mis-collected as C symbols, causing false "defined in multiple
8
+ * languages" conflicts against C-Next symbols like Arduino `loop()`.
9
+ * detectAssemblySyntax lets the header pipeline skip such files.
10
+ */
11
+ import { describe, it, expect } from "vitest";
12
+ import detectAssemblySyntax from "../detectAssemblySyntax";
13
+
14
+ describe("detectAssemblySyntax", () => {
15
+ describe("assembler sources - should return true", () => {
16
+ it("detects a GAS .macro block (xtensa coreasm.h shape)", () => {
17
+ const coreasm = [
18
+ "#define _ASMLANGUAGE",
19
+ "\t.macro\tfloop_\tar, startlabel, endlabelref",
20
+ "\tloop\t\\ar, \\endlabelref",
21
+ "\t.endm",
22
+ ].join("\n");
23
+ expect(detectAssemblySyntax(coreasm)).toBe(true);
24
+ });
25
+
26
+ it("detects the _ASMLANGUAGE marker on its own", () => {
27
+ expect(detectAssemblySyntax("#define _ASMLANGUAGE 1\n")).toBe(true);
28
+ });
29
+
30
+ it("detects section/global directives at line start", () => {
31
+ expect(detectAssemblySyntax(" .section .text\n .global _start\n")).toBe(
32
+ true,
33
+ );
34
+ });
35
+
36
+ it("detects .macro regardless of leading whitespace", () => {
37
+ expect(detectAssemblySyntax(" .macro find_ms_setbit ad, as\n")).toBe(
38
+ true,
39
+ );
40
+ });
41
+
42
+ it("detects a spaced '# define _ASMLANGUAGE' directive", () => {
43
+ expect(detectAssemblySyntax("# define _ASMLANGUAGE\n")).toBe(true);
44
+ });
45
+
46
+ it("detects portable assembly markers defined whole-file", () => {
47
+ expect(detectAssemblySyntax("#define __ASSEMBLY__ 1\n")).toBe(true);
48
+ expect(detectAssemblySyntax("#define __ASSEMBLER__\n")).toBe(true);
49
+ });
50
+ });
51
+
52
+ describe("C / C++ headers - should return false", () => {
53
+ it("returns false for a C function named loop", () => {
54
+ expect(
55
+ detectAssemblySyntax("void loop(void);\nint add(int a, int b);"),
56
+ ).toBe(false);
57
+ });
58
+
59
+ it("returns false for structs, enums, typedefs", () => {
60
+ expect(
61
+ detectAssemblySyntax(
62
+ "typedef struct { int x; } Point;\nenum Color { RED, GREEN };",
63
+ ),
64
+ ).toBe(false);
65
+ });
66
+
67
+ it("does not false-positive on member access or identifiers containing 'macro'", () => {
68
+ expect(
69
+ detectAssemblySyntax("int y = p.x;\nint n = cfg.macro_count;"),
70
+ ).toBe(false);
71
+ });
72
+
73
+ it("returns false for a C++ class header", () => {
74
+ expect(
75
+ detectAssemblySyntax("class Foo : public Bar { public: int x; };"),
76
+ ).toBe(false);
77
+ });
78
+
79
+ it("returns false for a C header that guards prototypes with #ifndef _ASMLANGUAGE", () => {
80
+ // xtensa xtruntime.h shape: raw-fallback content still holds the guard
81
+ // line. The C prototypes inside must NOT be dropped as assembler.
82
+ const xtruntime = [
83
+ "#ifndef _ASMLANGUAGE",
84
+ "extern void xthal_window_spill(void);",
85
+ "extern unsigned xthal_get_ccount(void);",
86
+ "#endif",
87
+ ].join("\n");
88
+ expect(detectAssemblySyntax(xtruntime)).toBe(false);
89
+ });
90
+
91
+ it("returns false for a C header guarded by #ifndef __ASSEMBLY__ / #ifdef __ASSEMBLER__", () => {
92
+ // Linux-kernel-style guards fence the C prototypes; on the raw-fallback
93
+ // path the guard lines survive but the file is still a normal C header.
94
+ const guarded = [
95
+ "#ifndef __ASSEMBLY__",
96
+ "extern int kernel_init(void);",
97
+ "#endif",
98
+ "#ifdef __ASSEMBLER__",
99
+ "/* asm-only region */",
100
+ "#endif",
101
+ ].join("\n");
102
+ expect(detectAssemblySyntax(guarded)).toBe(false);
103
+ });
104
+
105
+ it("does not match a longer macro name that merely starts with _ASMLANGUAGE", () => {
106
+ // Exact-token match: `#define _ASMLANGUAGE_CONFIG 1` is not the marker.
107
+ expect(detectAssemblySyntax("#define _ASMLANGUAGE_CONFIG 1\n")).toBe(
108
+ false,
109
+ );
110
+ });
111
+
112
+ it("returns false for empty content", () => {
113
+ expect(detectAssemblySyntax("")).toBe(false);
114
+ });
115
+ });
116
+ });
@@ -315,7 +315,10 @@ class FunctionCallListener extends CNextListener {
315
315
  if (!baseName) return;
316
316
 
317
317
  // Walk through postfix ops to find the call and resolve the name
318
- const { resolvedName, foundCall } = this.resolveCallTarget(ops, baseName);
318
+ const { resolvedName, foundCall, isGlobalCall } = this.resolveCallTarget(
319
+ ops,
320
+ baseName,
321
+ );
319
322
  if (!foundCall) return;
320
323
 
321
324
  // Check if the function is defined
@@ -325,6 +328,7 @@ class FunctionCallListener extends CNextListener {
325
328
  line,
326
329
  column,
327
330
  this.currentScope,
331
+ isGlobalCall,
328
332
  );
329
333
  };
330
334
 
@@ -340,6 +344,9 @@ class FunctionCallListener extends CNextListener {
340
344
  if (primary.THIS()) {
341
345
  return "this";
342
346
  }
347
+ if (primary.GLOBAL()) {
348
+ return "global";
349
+ }
343
350
  return null;
344
351
  }
345
352
 
@@ -351,15 +358,21 @@ class FunctionCallListener extends CNextListener {
351
358
  private resolveCallTarget(
352
359
  ops: Parser.PostfixOpContext[],
353
360
  baseName: string,
354
- ): { resolvedName: string; foundCall: boolean } {
361
+ ): { resolvedName: string; foundCall: boolean; isGlobalCall: boolean } {
355
362
  let resolvedName = baseName;
363
+ let isGlobalCall = baseName === "global";
356
364
 
357
365
  for (const op of ops) {
358
366
  // Member access: check if it's Scope.member or this.member pattern
359
367
  if (op.IDENTIFIER()) {
360
368
  const resolved = this.resolveMemberAccess(resolvedName, op);
361
369
  if (resolved === null) {
362
- return { resolvedName, foundCall: false };
370
+ return { resolvedName, foundCall: false, isGlobalCall };
371
+ }
372
+ // If resolution went through a known scope, this is a scope
373
+ // method call, not a global function lookup
374
+ if (isGlobalCall && this.analyzer.isScope(resolvedName)) {
375
+ isGlobalCall = false;
363
376
  }
364
377
  resolvedName = resolved;
365
378
  continue;
@@ -369,12 +382,12 @@ class FunctionCallListener extends CNextListener {
369
382
  if (op.argumentList() || op.getChildCount() === 2) {
370
383
  const text = op.getText();
371
384
  if (text.startsWith("(")) {
372
- return { resolvedName, foundCall: true };
385
+ return { resolvedName, foundCall: true, isGlobalCall };
373
386
  }
374
387
  }
375
388
  }
376
389
 
377
- return { resolvedName, foundCall: false };
390
+ return { resolvedName, foundCall: false, isGlobalCall };
378
391
  }
379
392
 
380
393
  /**
@@ -391,6 +404,11 @@ class FunctionCallListener extends CNextListener {
391
404
  return `${this.currentScope}_${memberName}`;
392
405
  }
393
406
 
407
+ // Issue #985: Handle global.member -> member (strip global prefix)
408
+ if (resolvedName === "global") {
409
+ return memberName;
410
+ }
411
+
394
412
  // Check if base is a known scope
395
413
  if (this.analyzer.isScope(resolvedName)) {
396
414
  return `${resolvedName}_${memberName}`;
@@ -934,12 +952,14 @@ class FunctionCallAnalyzer {
934
952
  * @param line Source line number
935
953
  * @param column Source column number
936
954
  * @param currentScope The current scope name (if inside a scope)
955
+ * @param isGlobalCall Whether the call used global. prefix
937
956
  */
938
957
  public checkFunctionCall(
939
958
  name: string,
940
959
  line: number,
941
960
  column: number,
942
961
  currentScope: string | null,
962
+ isGlobalCall: boolean = false,
943
963
  ): void {
944
964
  // Check for self-recursion (MISRA C:2012 Rule 17.2)
945
965
  if (this.currentFunctionName && name === this.currentFunctionName) {
@@ -981,7 +1001,8 @@ class FunctionCallAnalyzer {
981
1001
  // ADR-057: Allow implicit scope function calls without this. prefix
982
1002
  // Check if this is an unqualified call to a scope function
983
1003
  // e.g., calling helper() instead of this.helper() inside a scope
984
- if (currentScope) {
1004
+ // Skip for global. calls — global. explicitly means global scope
1005
+ if (currentScope && !isGlobalCall) {
985
1006
  const qualifiedName = `${currentScope}_${name}`;
986
1007
  if (this.definedFunctions.has(qualifiedName)) {
987
1008
  return; // OK - implicit resolution will handle it
@@ -989,11 +1010,15 @@ class FunctionCallAnalyzer {
989
1010
  }
990
1011
 
991
1012
  // Not defined - report error with optional hint
1013
+ // If the function is local (defined later in this file), it's a
1014
+ // define-before-use error regardless of global. prefix
1015
+ const isLocalFunction = this.allLocalFunctions.has(name);
992
1016
  const header = this.findStdlibHeader(name);
993
- let message = `function '${name}' called before definition`;
994
- if (header) {
995
- message += `; hint: '${name}' is available from ${header} — try global.${name}()`;
996
- }
1017
+ const message = this.buildUndefinedFunctionMessage(
1018
+ name,
1019
+ header,
1020
+ isGlobalCall && !isLocalFunction,
1021
+ );
997
1022
 
998
1023
  this.errors.push({
999
1024
  code: "E0422",
@@ -1004,6 +1029,28 @@ class FunctionCallAnalyzer {
1004
1029
  });
1005
1030
  }
1006
1031
 
1032
+ /**
1033
+ * Issue #985: Build error message for undefined function calls.
1034
+ * Adjusts hint based on whether the call used global. prefix.
1035
+ */
1036
+ private buildUndefinedFunctionMessage(
1037
+ name: string,
1038
+ header: string | null,
1039
+ isGlobalCall: boolean,
1040
+ ): string {
1041
+ if (isGlobalCall && header) {
1042
+ return `'${name}' is not declared in any included header; add #include <${header}>`;
1043
+ }
1044
+ if (isGlobalCall) {
1045
+ return `'${name}' is not declared in any included header`;
1046
+ }
1047
+ let message = `function '${name}' called before definition`;
1048
+ if (header) {
1049
+ message += `; hint: '${name}' is available from ${header} — try global.${name}()`;
1050
+ }
1051
+ return message;
1052
+ }
1053
+
1007
1054
  /**
1008
1055
  * Check if a function is defined externally (from included files)
1009
1056
  * This includes C/C++ headers AND C-Next includes.
@@ -1033,6 +1080,14 @@ class FunctionCallAnalyzer {
1033
1080
  }
1034
1081
  }
1035
1082
 
1083
+ // Issue #985 recovery: a function-like MACRO recovered by translation-unit
1084
+ // preprocessing (e.g. FreeRTOS pdMS_TO_TICKS). Recovered functions are
1085
+ // registered as full symbols (found above via getOverloads); macros have no
1086
+ // declaration to parse, so only their name is known.
1087
+ if (this.symbolTable.hasExternalDeclaration(name)) {
1088
+ return true;
1089
+ }
1090
+
1036
1091
  return false;
1037
1092
  }
1038
1093
  }