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.
- package/README.md +18 -2
- package/dist/index.js +8897 -6260
- package/dist/index.js.map +4 -4
- package/grammar/CNext.g4 +12 -0
- package/package.json +4 -2
- package/src/transpiler/Transpiler.ts +376 -48
- package/src/transpiler/__tests__/DualCodePaths.test.ts +1 -1
- 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 +65 -10
- package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +186 -14
- 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/SignedShiftAnalyzer.ts +124 -12
- package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +200 -0
- package/src/transpiler/logic/analysis/__tests__/InitializationAnalyzer.test.ts +386 -1
- 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/__tests__/SignedShiftAnalyzer.test.ts +211 -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 +1318 -1178
- 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 +62 -2
- package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +55 -4
- package/src/transpiler/logic/symbols/cnext/collectors/VariableCollector.ts +15 -2
- package/src/transpiler/logic/symbols/cnext/utils/TypeUtils.ts +64 -50
- 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 +196 -98
- 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.coverage.test.ts +165 -17
- package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +163 -35
- package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +2 -2
- 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 +97 -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/declarationGenerators/ScopeGenerator.ts +26 -7
- package/src/transpiler/output/codegen/generators/declarationGenerators/__tests__/ScopeGenerator.test.ts +86 -0
- package/src/transpiler/output/codegen/generators/expressions/BinaryExprGenerator.ts +43 -24
- package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +76 -58
- package/src/transpiler/output/codegen/generators/expressions/ExpressionGenerator.ts +9 -2
- package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +26 -13
- package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +44 -0
- package/src/transpiler/output/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts +82 -1
- 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/ParameterInputAdapter.ts +17 -3
- package/src/transpiler/output/codegen/helpers/ParameterSignatureBuilder.ts +17 -4
- package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +240 -42
- package/src/transpiler/output/codegen/helpers/SymbolLookupHelper.ts +0 -21
- package/src/transpiler/output/codegen/helpers/TypeGenerationHelper.ts +60 -39
- package/src/transpiler/output/codegen/helpers/TypeRegistrationEngine.ts +170 -36
- package/src/transpiler/output/codegen/helpers/VariableDeclHelper.ts +37 -39
- package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +19 -0
- package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +117 -0
- package/src/transpiler/output/codegen/helpers/__tests__/ParameterSignatureBuilder.test.ts +94 -2
- package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +322 -9
- package/src/transpiler/output/codegen/helpers/__tests__/SymbolLookupHelper.test.ts +0 -64
- package/src/transpiler/output/codegen/helpers/__tests__/TypeRegistrationEngine.test.ts +101 -0
- package/src/transpiler/output/codegen/helpers/__tests__/VariableDeclHelper.test.ts +29 -5
- 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/codegen/types/ICallbackTypeInfo.ts +2 -1
- package/src/transpiler/output/codegen/types/IParameterInput.ts +7 -0
- package/src/transpiler/output/headers/BaseHeaderGenerator.ts +8 -0
- package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +140 -24
- package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +280 -0
- package/src/transpiler/output/headers/generators/IHeaderTypeInput.ts +13 -0
- package/src/transpiler/output/headers/generators/generateStructHeader.ts +48 -28
- package/src/transpiler/state/CodeGenState.ts +91 -22
- package/src/transpiler/state/__tests__/CodeGenState.test.ts +253 -11
- package/src/transpiler/types/ICachedFileEntry.ts +7 -0
- package/src/utils/LiteralUtils.ts +23 -0
- package/src/utils/__tests__/LiteralUtils.test.ts +101 -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/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(
|
|
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
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
}
|