c-next 0.1.67 → 0.1.68
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/package.json +1 -1
- package/src/transpiler/Transpiler.ts +5 -13
- package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +41 -36
- package/src/transpiler/logic/analysis/__tests__/InitializationAnalyzer.test.ts +29 -28
- package/src/transpiler/logic/analysis/__tests__/runAnalyzers.test.ts +32 -19
- package/src/transpiler/logic/analysis/runAnalyzers.ts +8 -14
- package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +31 -10
- package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +66 -4
- package/src/transpiler/output/codegen/helpers/MemberAccessValidator.ts +47 -6
- package/src/transpiler/output/codegen/helpers/__tests__/MemberAccessValidator.test.ts +109 -3
- package/src/transpiler/state/CodeGenState.ts +40 -0
- package/src/transpiler/state/__tests__/CodeGenState.test.ts +119 -0
- package/src/transpiler/logic/analysis/AnalyzerContextBuilder.ts +0 -58
- package/src/transpiler/logic/analysis/__tests__/AnalyzerContextBuilder.test.ts +0 -137
package/package.json
CHANGED
|
@@ -53,7 +53,6 @@ import ITranspileError from "../lib/types/ITranspileError";
|
|
|
53
53
|
import TranspilerState from "./types/TranspilerState";
|
|
54
54
|
import runAnalyzers from "./logic/analysis/runAnalyzers";
|
|
55
55
|
import ModificationAnalyzer from "./logic/analysis/ModificationAnalyzer";
|
|
56
|
-
import AnalyzerContextBuilder from "./logic/analysis/AnalyzerContextBuilder";
|
|
57
56
|
import CacheManager from "../utils/cache/CacheManager";
|
|
58
57
|
import MapUtils from "../utils/MapUtils";
|
|
59
58
|
import detectCppSyntax from "./logic/detectCppSyntax";
|
|
@@ -247,7 +246,7 @@ class Transpiler {
|
|
|
247
246
|
*
|
|
248
247
|
* Both run() and transpileSource() delegate here after file discovery.
|
|
249
248
|
*
|
|
250
|
-
* Stage 2: Collect symbols from C/C++ headers
|
|
249
|
+
* Stage 2: Collect symbols from C/C++ headers (includes building analyzer context)
|
|
251
250
|
* Stage 3: Collect symbols from C-Next files
|
|
252
251
|
* Stage 3b: Resolve external const array dimensions
|
|
253
252
|
* Stage 4: Check for symbol conflicts
|
|
@@ -258,8 +257,9 @@ class Transpiler {
|
|
|
258
257
|
input: IPipelineInput,
|
|
259
258
|
result: ITranspilerResult,
|
|
260
259
|
): Promise<void> {
|
|
261
|
-
// Stage 2: Collect symbols from C/C++ headers
|
|
260
|
+
// Stage 2: Collect symbols from C/C++ headers and build analyzer context
|
|
262
261
|
this._collectAllHeaderSymbols(input.headerFiles, result);
|
|
262
|
+
CodeGenState.buildExternalStructFields();
|
|
263
263
|
|
|
264
264
|
// Stage 3: Collect symbols from C-Next files
|
|
265
265
|
if (!this._collectAllCNextSymbolsFromPipeline(input.cnextFiles, result)) {
|
|
@@ -395,16 +395,8 @@ class Transpiler {
|
|
|
395
395
|
return this.buildParseOnlyResult(sourcePath, declarationCount);
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
// Run analyzers
|
|
399
|
-
const
|
|
400
|
-
AnalyzerContextBuilder.buildExternalStructFields(
|
|
401
|
-
CodeGenState.symbolTable,
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
const analyzerErrors = runAnalyzers(tree, tokenStream, {
|
|
405
|
-
externalStructFields,
|
|
406
|
-
symbolTable: CodeGenState.symbolTable,
|
|
407
|
-
});
|
|
398
|
+
// Run analyzers (reads externalStructFields and symbolTable from CodeGenState)
|
|
399
|
+
const analyzerErrors = runAnalyzers(tree, tokenStream);
|
|
408
400
|
if (analyzerErrors.length > 0) {
|
|
409
401
|
return this.buildErrorResult(
|
|
410
402
|
sourcePath,
|
|
@@ -21,6 +21,7 @@ import ExpressionUtils from "../../../utils/ExpressionUtils";
|
|
|
21
21
|
import ParserUtils from "../../../utils/ParserUtils";
|
|
22
22
|
import analyzePostfixOps from "../../../utils/PostfixAnalysisUtils";
|
|
23
23
|
import SymbolTable from "../symbols/SymbolTable";
|
|
24
|
+
import CodeGenState from "../../state/CodeGenState";
|
|
24
25
|
import ESourceLanguage from "../../../utils/types/ESourceLanguage";
|
|
25
26
|
import ESymbolKind from "../../../utils/types/ESymbolKind";
|
|
26
27
|
|
|
@@ -408,8 +409,11 @@ class InitializationAnalyzer {
|
|
|
408
409
|
|
|
409
410
|
private scopeStack: ScopeStack<IVariableState> = new ScopeStack();
|
|
410
411
|
|
|
411
|
-
/**
|
|
412
|
-
|
|
412
|
+
/**
|
|
413
|
+
* C-Next struct fields from current file (collected at analysis time).
|
|
414
|
+
* External struct fields from C/C++ headers are accessed via CodeGenState.
|
|
415
|
+
*/
|
|
416
|
+
private cnextStructFields: Map<string, Set<string>> = new Map();
|
|
413
417
|
|
|
414
418
|
/** Track if we're processing a write target (left side of assignment) */
|
|
415
419
|
private inWriteContext: boolean = false;
|
|
@@ -418,17 +422,25 @@ class InitializationAnalyzer {
|
|
|
418
422
|
private symbolTable: SymbolTable | null = null;
|
|
419
423
|
|
|
420
424
|
/**
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* @param externalFields Map of struct name -> Set of field names
|
|
425
|
+
* Get struct fields for a given struct type.
|
|
426
|
+
* Checks C-Next structs first, then falls back to CodeGenState for external structs.
|
|
425
427
|
*/
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
428
|
+
private getStructFields(structName: string): Set<string> | undefined {
|
|
429
|
+
// Check C-Next structs from current file first
|
|
430
|
+
const cnextFields = this.cnextStructFields.get(structName);
|
|
431
|
+
if (cnextFields) {
|
|
432
|
+
return cnextFields;
|
|
431
433
|
}
|
|
434
|
+
|
|
435
|
+
// Check external structs from CodeGenState
|
|
436
|
+
return CodeGenState.getExternalStructFields().get(structName);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Check if a type name is a known struct.
|
|
441
|
+
*/
|
|
442
|
+
private isKnownStruct(typeName: string): boolean {
|
|
443
|
+
return this.getStructFields(typeName) !== undefined;
|
|
432
444
|
}
|
|
433
445
|
|
|
434
446
|
/**
|
|
@@ -469,9 +481,10 @@ class InitializationAnalyzer {
|
|
|
469
481
|
this.errors = [];
|
|
470
482
|
this.scopeStack = new ScopeStack();
|
|
471
483
|
this.symbolTable = symbolTable ?? null;
|
|
472
|
-
//
|
|
484
|
+
// Clear C-Next struct fields from previous analysis (external fields come from CodeGenState)
|
|
485
|
+
this.cnextStructFields = new Map();
|
|
473
486
|
|
|
474
|
-
// First pass: collect struct definitions
|
|
487
|
+
// First pass: collect struct definitions from current file
|
|
475
488
|
this.collectStructDefinitions(tree);
|
|
476
489
|
|
|
477
490
|
// Create global scope with all global/namespace variable declarations
|
|
@@ -553,7 +566,8 @@ class InitializationAnalyzer {
|
|
|
553
566
|
}
|
|
554
567
|
|
|
555
568
|
/**
|
|
556
|
-
* Collect struct definitions to know their fields
|
|
569
|
+
* Collect struct definitions from current file to know their fields.
|
|
570
|
+
* This supplements the external struct fields from CodeGenState.
|
|
557
571
|
*/
|
|
558
572
|
private collectStructDefinitions(tree: Parser.ProgramContext): void {
|
|
559
573
|
for (const decl of tree.declaration()) {
|
|
@@ -567,7 +581,7 @@ class InitializationAnalyzer {
|
|
|
567
581
|
fields.add(fieldName);
|
|
568
582
|
}
|
|
569
583
|
|
|
570
|
-
this.
|
|
584
|
+
this.cnextStructFields.set(structName, fields);
|
|
571
585
|
}
|
|
572
586
|
}
|
|
573
587
|
}
|
|
@@ -610,9 +624,9 @@ class InitializationAnalyzer {
|
|
|
610
624
|
this.enterScope();
|
|
611
625
|
}
|
|
612
626
|
|
|
613
|
-
const isStruct = typeName !== null && this.
|
|
627
|
+
const isStruct = typeName !== null && this.isKnownStruct(typeName);
|
|
614
628
|
const fields = isStruct
|
|
615
|
-
? this.
|
|
629
|
+
? this.getStructFields(typeName)!
|
|
616
630
|
: new Set<string>();
|
|
617
631
|
|
|
618
632
|
// Issue #503: C++ classes with default constructors are automatically initialized
|
|
@@ -637,13 +651,11 @@ class InitializationAnalyzer {
|
|
|
637
651
|
* SonarCloud S3776: Refactored to use helper methods.
|
|
638
652
|
*/
|
|
639
653
|
public recordAssignment(name: string, field?: string): void {
|
|
640
|
-
const structFields = this.structFields;
|
|
641
|
-
|
|
642
654
|
this.scopeStack.update(name, (state) => {
|
|
643
655
|
if (field) {
|
|
644
|
-
this.recordFieldAssignment(state, field
|
|
656
|
+
this.recordFieldAssignment(state, field);
|
|
645
657
|
} else {
|
|
646
|
-
this.recordWholeAssignment(state
|
|
658
|
+
this.recordWholeAssignment(state);
|
|
647
659
|
}
|
|
648
660
|
return state;
|
|
649
661
|
});
|
|
@@ -652,16 +664,12 @@ class InitializationAnalyzer {
|
|
|
652
664
|
/**
|
|
653
665
|
* Handle field-level assignment.
|
|
654
666
|
*/
|
|
655
|
-
private recordFieldAssignment(
|
|
656
|
-
state: IVariableState,
|
|
657
|
-
field: string,
|
|
658
|
-
structFields: Map<string, Set<string>>,
|
|
659
|
-
): void {
|
|
667
|
+
private recordFieldAssignment(state: IVariableState, field: string): void {
|
|
660
668
|
state.initializedFields.add(field);
|
|
661
669
|
// Check if all fields are now initialized
|
|
662
670
|
if (!state.isStruct || !state.typeName) return;
|
|
663
671
|
|
|
664
|
-
const allFields =
|
|
672
|
+
const allFields = this.getStructFields(state.typeName);
|
|
665
673
|
if (!allFields) return;
|
|
666
674
|
|
|
667
675
|
const allInitialized = [...allFields].every((f) =>
|
|
@@ -675,15 +683,12 @@ class InitializationAnalyzer {
|
|
|
675
683
|
/**
|
|
676
684
|
* Handle whole-variable assignment.
|
|
677
685
|
*/
|
|
678
|
-
private recordWholeAssignment(
|
|
679
|
-
state: IVariableState,
|
|
680
|
-
structFields: Map<string, Set<string>>,
|
|
681
|
-
): void {
|
|
686
|
+
private recordWholeAssignment(state: IVariableState): void {
|
|
682
687
|
state.initialized = true;
|
|
683
688
|
// Mark all fields as initialized too
|
|
684
689
|
if (!state.isStruct || !state.typeName) return;
|
|
685
690
|
|
|
686
|
-
const fields =
|
|
691
|
+
const fields = this.getStructFields(state.typeName);
|
|
687
692
|
if (fields) {
|
|
688
693
|
state.initializedFields = new Set(fields);
|
|
689
694
|
}
|
|
@@ -745,8 +750,8 @@ class InitializationAnalyzer {
|
|
|
745
750
|
field: string,
|
|
746
751
|
state: IVariableState,
|
|
747
752
|
): void {
|
|
748
|
-
const
|
|
749
|
-
if (!
|
|
753
|
+
const structFieldSet = this.getStructFields(state.typeName!);
|
|
754
|
+
if (!structFieldSet?.has(field)) return;
|
|
750
755
|
|
|
751
756
|
if (!state.initializedFields.has(field)) {
|
|
752
757
|
this.addError(`${name}.${field}`, line, column, state.declaration, false);
|
|
@@ -874,9 +879,9 @@ class InitializationAnalyzer {
|
|
|
874
879
|
this.enterScope();
|
|
875
880
|
}
|
|
876
881
|
|
|
877
|
-
const isStruct = typeName !== null && this.
|
|
882
|
+
const isStruct = typeName !== null && this.isKnownStruct(typeName);
|
|
878
883
|
const fields = isStruct
|
|
879
|
-
? this.
|
|
884
|
+
? this.getStructFields(typeName)!
|
|
880
885
|
: new Set<string>();
|
|
881
886
|
|
|
882
887
|
const state: IVariableState = {
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { CharStream, CommonTokenStream } from "antlr4ng";
|
|
7
|
-
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { describe, expect, it, beforeEach } from "vitest";
|
|
8
8
|
import { CNextLexer } from "../../parser/grammar/CNextLexer";
|
|
9
9
|
import { CNextParser } from "../../parser/grammar/CNextParser";
|
|
10
10
|
import InitializationAnalyzer from "../InitializationAnalyzer";
|
|
11
11
|
import SymbolTable from "../../symbols/SymbolTable";
|
|
12
|
+
import CodeGenState from "../../../state/CodeGenState";
|
|
12
13
|
import ESymbolKind from "../../../../utils/types/ESymbolKind";
|
|
13
14
|
import ESourceLanguage from "../../../../utils/types/ESourceLanguage";
|
|
14
15
|
|
|
@@ -24,6 +25,12 @@ function parse(source: string) {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
describe("InitializationAnalyzer", () => {
|
|
28
|
+
// Reset CodeGenState before each test
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
CodeGenState.reset();
|
|
31
|
+
CodeGenState.symbolTable.clear();
|
|
32
|
+
});
|
|
33
|
+
|
|
27
34
|
// ========================================================================
|
|
28
35
|
// Issue #503: C++ Class Initialization
|
|
29
36
|
// ========================================================================
|
|
@@ -38,9 +45,8 @@ describe("InitializationAnalyzer", () => {
|
|
|
38
45
|
`;
|
|
39
46
|
const tree = parse(code);
|
|
40
47
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
symbolTable.addSymbol({
|
|
48
|
+
// Set up CodeGenState with C++ class
|
|
49
|
+
CodeGenState.symbolTable.addSymbol({
|
|
44
50
|
name: "CppMessage",
|
|
45
51
|
kind: ESymbolKind.Class,
|
|
46
52
|
sourceLanguage: ESourceLanguage.Cpp,
|
|
@@ -49,14 +55,13 @@ describe("InitializationAnalyzer", () => {
|
|
|
49
55
|
isExported: true,
|
|
50
56
|
});
|
|
51
57
|
// Add the field so the analyzer knows about it
|
|
52
|
-
symbolTable.addStructField("CppMessage", "pgn", "u16");
|
|
58
|
+
CodeGenState.symbolTable.addStructField("CppMessage", "pgn", "u16");
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
new Map([["CppMessage", new Set(["pgn"])]]),
|
|
57
|
-
);
|
|
60
|
+
// Build external struct fields from symbol table
|
|
61
|
+
CodeGenState.buildExternalStructFields();
|
|
58
62
|
|
|
59
|
-
const
|
|
63
|
+
const analyzer = new InitializationAnalyzer();
|
|
64
|
+
const errors = analyzer.analyze(tree, CodeGenState.symbolTable);
|
|
60
65
|
|
|
61
66
|
// Should have NO errors - C++ class is initialized by constructor
|
|
62
67
|
expect(errors).toHaveLength(0);
|
|
@@ -95,9 +100,8 @@ describe("InitializationAnalyzer", () => {
|
|
|
95
100
|
`;
|
|
96
101
|
const tree = parse(code);
|
|
97
102
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
symbolTable.addSymbol({
|
|
103
|
+
// Set up CodeGenState with C++ struct (not class)
|
|
104
|
+
CodeGenState.symbolTable.addSymbol({
|
|
101
105
|
name: "CppStruct",
|
|
102
106
|
kind: ESymbolKind.Struct,
|
|
103
107
|
sourceLanguage: ESourceLanguage.Cpp,
|
|
@@ -105,14 +109,13 @@ describe("InitializationAnalyzer", () => {
|
|
|
105
109
|
sourceLine: 1,
|
|
106
110
|
isExported: true,
|
|
107
111
|
});
|
|
108
|
-
symbolTable.addStructField("CppStruct", "value", "u32");
|
|
112
|
+
CodeGenState.symbolTable.addStructField("CppStruct", "value", "u32");
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
new Map([["CppStruct", new Set(["value"])]]),
|
|
113
|
-
);
|
|
114
|
+
// Build external struct fields from symbol table
|
|
115
|
+
CodeGenState.buildExternalStructFields();
|
|
114
116
|
|
|
115
|
-
const
|
|
117
|
+
const analyzer = new InitializationAnalyzer();
|
|
118
|
+
const errors = analyzer.analyze(tree, CodeGenState.symbolTable);
|
|
116
119
|
|
|
117
120
|
// Should have NO errors - C++ structs also have default constructors
|
|
118
121
|
expect(errors).toHaveLength(0);
|
|
@@ -127,9 +130,8 @@ describe("InitializationAnalyzer", () => {
|
|
|
127
130
|
`;
|
|
128
131
|
const tree = parse(code);
|
|
129
132
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
symbolTable.addSymbol({
|
|
133
|
+
// Set up CodeGenState with C struct (not C++)
|
|
134
|
+
CodeGenState.symbolTable.addSymbol({
|
|
133
135
|
name: "CStruct",
|
|
134
136
|
kind: ESymbolKind.Struct,
|
|
135
137
|
sourceLanguage: ESourceLanguage.C,
|
|
@@ -137,14 +139,13 @@ describe("InitializationAnalyzer", () => {
|
|
|
137
139
|
sourceLine: 1,
|
|
138
140
|
isExported: true,
|
|
139
141
|
});
|
|
140
|
-
symbolTable.addStructField("CStruct", "value", "u32");
|
|
142
|
+
CodeGenState.symbolTable.addStructField("CStruct", "value", "u32");
|
|
141
143
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
new Map([["CStruct", new Set(["value"])]]),
|
|
145
|
-
);
|
|
144
|
+
// Build external struct fields from symbol table (as pipeline does)
|
|
145
|
+
CodeGenState.buildExternalStructFields();
|
|
146
146
|
|
|
147
|
-
const
|
|
147
|
+
const analyzer = new InitializationAnalyzer();
|
|
148
|
+
const errors = analyzer.analyze(tree, CodeGenState.symbolTable);
|
|
148
149
|
|
|
149
150
|
// SHOULD have an error - C structs don't have constructors
|
|
150
151
|
expect(errors.length).toBeGreaterThan(0);
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Unit tests for runAnalyzers
|
|
3
3
|
* Tests that all 8 analyzers run in sequence with early returns on errors
|
|
4
4
|
*/
|
|
5
|
-
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
6
6
|
import { CharStream, CommonTokenStream } from "antlr4ng";
|
|
7
7
|
import { CNextLexer } from "../../parser/grammar/CNextLexer";
|
|
8
8
|
import { CNextParser } from "../../parser/grammar/CNextParser";
|
|
9
9
|
import runAnalyzers from "../runAnalyzers";
|
|
10
10
|
import SymbolTable from "../../symbols/SymbolTable";
|
|
11
|
+
import CodeGenState from "../../../state/CodeGenState";
|
|
11
12
|
import ESymbolKind from "../../../../utils/types/ESymbolKind";
|
|
12
13
|
import ESourceLanguage from "../../../../utils/types/ESourceLanguage";
|
|
13
14
|
|
|
@@ -24,6 +25,12 @@ function parseWithStream(source: string) {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
describe("runAnalyzers", () => {
|
|
28
|
+
// Reset CodeGenState before each test
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
CodeGenState.reset();
|
|
31
|
+
CodeGenState.symbolTable.clear();
|
|
32
|
+
});
|
|
33
|
+
|
|
27
34
|
// ========================================================================
|
|
28
35
|
// Happy Path
|
|
29
36
|
// ========================================================================
|
|
@@ -206,24 +213,33 @@ describe("runAnalyzers", () => {
|
|
|
206
213
|
});
|
|
207
214
|
|
|
208
215
|
// ========================================================================
|
|
209
|
-
// Options:
|
|
216
|
+
// Options: CodeGenState integration and symbolTable
|
|
210
217
|
// ========================================================================
|
|
211
218
|
|
|
212
219
|
describe("options", () => {
|
|
213
|
-
it("should
|
|
214
|
-
// Code that uses a field from an external struct -
|
|
215
|
-
//
|
|
220
|
+
it("should read externalStructFields from CodeGenState", () => {
|
|
221
|
+
// Code that uses a field from an external struct - externalStructFields
|
|
222
|
+
// are now read from CodeGenState
|
|
216
223
|
const { tree, tokenStream } = parseWithStream(`
|
|
217
224
|
void main() {
|
|
218
225
|
u32 x <- 5;
|
|
219
226
|
}
|
|
220
227
|
`);
|
|
221
228
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
229
|
+
// Set up external struct fields in CodeGenState
|
|
230
|
+
CodeGenState.symbolTable.addStructField(
|
|
231
|
+
"ExternalStruct",
|
|
232
|
+
"field1",
|
|
233
|
+
"u32",
|
|
234
|
+
);
|
|
235
|
+
CodeGenState.symbolTable.addStructField(
|
|
236
|
+
"ExternalStruct",
|
|
237
|
+
"field2",
|
|
238
|
+
"u32",
|
|
239
|
+
);
|
|
240
|
+
CodeGenState.buildExternalStructFields();
|
|
225
241
|
|
|
226
|
-
const errors = runAnalyzers(tree, tokenStream
|
|
242
|
+
const errors = runAnalyzers(tree, tokenStream);
|
|
227
243
|
expect(errors).toHaveLength(0);
|
|
228
244
|
});
|
|
229
245
|
|
|
@@ -248,18 +264,15 @@ describe("runAnalyzers", () => {
|
|
|
248
264
|
expect(errors).toHaveLength(0);
|
|
249
265
|
});
|
|
250
266
|
|
|
251
|
-
it("should
|
|
267
|
+
it("should use CodeGenState.symbolTable by default", () => {
|
|
252
268
|
const { tree, tokenStream } = parseWithStream(`
|
|
253
269
|
void main() {
|
|
254
270
|
u32 x <- 5;
|
|
255
271
|
}
|
|
256
272
|
`);
|
|
257
273
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
]);
|
|
261
|
-
const symbolTable = new SymbolTable();
|
|
262
|
-
symbolTable.addSymbol({
|
|
274
|
+
// Set up C++ class in CodeGenState.symbolTable
|
|
275
|
+
CodeGenState.symbolTable.addSymbol({
|
|
263
276
|
name: "CppMessage",
|
|
264
277
|
kind: ESymbolKind.Class,
|
|
265
278
|
sourceLanguage: ESourceLanguage.Cpp,
|
|
@@ -267,11 +280,11 @@ describe("runAnalyzers", () => {
|
|
|
267
280
|
sourceLine: 1,
|
|
268
281
|
isExported: true,
|
|
269
282
|
});
|
|
283
|
+
CodeGenState.symbolTable.addStructField("CppMessage", "pgn", "u16");
|
|
284
|
+
CodeGenState.buildExternalStructFields();
|
|
270
285
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
symbolTable,
|
|
274
|
-
});
|
|
286
|
+
// No options passed - should use CodeGenState.symbolTable
|
|
287
|
+
const errors = runAnalyzers(tree, tokenStream);
|
|
275
288
|
expect(errors).toHaveLength(0);
|
|
276
289
|
});
|
|
277
290
|
});
|
|
@@ -17,20 +17,16 @@ import FloatModuloAnalyzer from "./FloatModuloAnalyzer";
|
|
|
17
17
|
import CommentExtractor from "./CommentExtractor";
|
|
18
18
|
import ITranspileError from "../../../lib/types/ITranspileError";
|
|
19
19
|
import SymbolTable from "../symbols/SymbolTable";
|
|
20
|
+
import CodeGenState from "../../state/CodeGenState";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Options for running analyzers
|
|
23
24
|
*/
|
|
24
25
|
interface IAnalyzerOptions {
|
|
25
|
-
/**
|
|
26
|
-
* External struct field information from C/C++ headers
|
|
27
|
-
* Maps struct name -> Set of field names
|
|
28
|
-
*/
|
|
29
|
-
externalStructFields?: Map<string, Set<string>>;
|
|
30
|
-
|
|
31
26
|
/**
|
|
32
27
|
* Symbol table containing external function definitions from C/C++ headers
|
|
33
|
-
* Used by FunctionCallAnalyzer to recognize external functions
|
|
28
|
+
* Used by FunctionCallAnalyzer to recognize external functions.
|
|
29
|
+
* Falls back to CodeGenState.symbolTable if not provided.
|
|
34
30
|
*/
|
|
35
31
|
symbolTable?: SymbolTable;
|
|
36
32
|
}
|
|
@@ -100,13 +96,11 @@ function runAnalyzers(
|
|
|
100
96
|
|
|
101
97
|
// 3. Initialization analysis (Rust-style use-before-init detection)
|
|
102
98
|
const initAnalyzer = new InitializationAnalyzer();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
// Issue #503: Pass symbol table so C++ classes with default constructors are recognized
|
|
99
|
+
// External struct fields and symbolTable are read from CodeGenState directly
|
|
100
|
+
const symbolTable = options?.symbolTable ?? CodeGenState.symbolTable;
|
|
107
101
|
if (
|
|
108
102
|
collectErrors(
|
|
109
|
-
initAnalyzer.analyze(tree,
|
|
103
|
+
initAnalyzer.analyze(tree, symbolTable),
|
|
110
104
|
errors,
|
|
111
105
|
formatWithCode,
|
|
112
106
|
)
|
|
@@ -115,10 +109,10 @@ function runAnalyzers(
|
|
|
115
109
|
}
|
|
116
110
|
|
|
117
111
|
// 4. Call analysis (ADR-030: define-before-use)
|
|
118
|
-
const
|
|
112
|
+
const callAnalyzer = new FunctionCallAnalyzer();
|
|
119
113
|
if (
|
|
120
114
|
collectErrors(
|
|
121
|
-
|
|
115
|
+
callAnalyzer.analyze(tree, symbolTable),
|
|
122
116
|
errors,
|
|
123
117
|
formatWithCode,
|
|
124
118
|
)
|
|
@@ -1441,16 +1441,33 @@ const generateMemberAccess = (
|
|
|
1441
1441
|
state: IGeneratorState,
|
|
1442
1442
|
orchestrator: IOrchestrator,
|
|
1443
1443
|
effects: TGeneratorEffect[],
|
|
1444
|
-
): MemberAccessResult =>
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1444
|
+
): MemberAccessResult => {
|
|
1445
|
+
// Check for enum shadowing before dispatch - catches case where identifier
|
|
1446
|
+
// was resolved to a scope member that shadows a global enum
|
|
1447
|
+
MemberAccessValidator.validateGlobalEntityAccess(
|
|
1448
|
+
ctx.result,
|
|
1449
|
+
ctx.memberName,
|
|
1450
|
+
"enum",
|
|
1451
|
+
state.currentScope,
|
|
1452
|
+
ctx.isGlobalAccess,
|
|
1453
|
+
{
|
|
1454
|
+
rootIdentifier: ctx.rootIdentifier,
|
|
1455
|
+
knownEnums: input.symbols!.knownEnums,
|
|
1456
|
+
},
|
|
1457
|
+
);
|
|
1458
|
+
|
|
1459
|
+
return (
|
|
1460
|
+
tryBitmapFieldAccess(ctx, input, effects) ??
|
|
1461
|
+
tryScopeMemberAccess(ctx, input, state, orchestrator) ??
|
|
1462
|
+
tryKnownScopeAccess(ctx, input, state, orchestrator) ??
|
|
1463
|
+
tryEnumMemberAccess(ctx, input, state, orchestrator) ??
|
|
1464
|
+
tryRegisterMemberAccess(ctx, input, state) ??
|
|
1465
|
+
tryStructParamAccess(ctx, orchestrator) ??
|
|
1466
|
+
tryRegisterBitmapAccess(ctx, input, effects) ??
|
|
1467
|
+
tryStructBitmapAccess(ctx, input, effects) ??
|
|
1468
|
+
generateDefaultAccess(ctx, orchestrator)
|
|
1469
|
+
);
|
|
1470
|
+
};
|
|
1454
1471
|
|
|
1455
1472
|
// ========================================================================
|
|
1456
1473
|
// Member Access Handlers
|
|
@@ -1575,12 +1592,15 @@ const tryEnumMemberAccess = (
|
|
|
1575
1592
|
return null;
|
|
1576
1593
|
}
|
|
1577
1594
|
|
|
1595
|
+
// Shadowing check already done in generateMemberAccess; this catches
|
|
1596
|
+
// direct conflicts where ctx.result is the enum name (no resolution happened)
|
|
1578
1597
|
MemberAccessValidator.validateGlobalEntityAccess(
|
|
1579
1598
|
ctx.result,
|
|
1580
1599
|
ctx.memberName,
|
|
1581
1600
|
"enum",
|
|
1582
1601
|
state.currentScope,
|
|
1583
1602
|
ctx.isGlobalAccess,
|
|
1603
|
+
{ scopeMembers: state.scopeMembers },
|
|
1584
1604
|
);
|
|
1585
1605
|
|
|
1586
1606
|
const output = initializeMemberOutput(ctx);
|
|
@@ -1606,6 +1626,7 @@ const tryRegisterMemberAccess = (
|
|
|
1606
1626
|
"register",
|
|
1607
1627
|
state.currentScope,
|
|
1608
1628
|
ctx.isGlobalAccess,
|
|
1629
|
+
{ scopeMembers: state.scopeMembers },
|
|
1609
1630
|
);
|
|
1610
1631
|
|
|
1611
1632
|
MemberAccessValidator.validateRegisterReadAccess(
|
|
@@ -1318,7 +1318,7 @@ describe("PostfixExpressionGenerator", () => {
|
|
|
1318
1318
|
expect(result.code).toBe("Color_Red");
|
|
1319
1319
|
});
|
|
1320
1320
|
|
|
1321
|
-
it("throws when accessing enum
|
|
1321
|
+
it("throws when accessing enum with naming conflict inside scope", () => {
|
|
1322
1322
|
const symbols = createMockSymbols({
|
|
1323
1323
|
knownEnums: new Set(["Color"]),
|
|
1324
1324
|
});
|
|
@@ -1326,7 +1326,8 @@ describe("PostfixExpressionGenerator", () => {
|
|
|
1326
1326
|
createMockPostfixOp({ identifier: "Red" }),
|
|
1327
1327
|
]);
|
|
1328
1328
|
const input = createMockInput({ symbols });
|
|
1329
|
-
const
|
|
1329
|
+
const scopeMembers = new Map([["Motor", new Set(["Color"])]]);
|
|
1330
|
+
const state = createMockState({ currentScope: "Motor", scopeMembers });
|
|
1330
1331
|
const orchestrator = createMockOrchestrator({
|
|
1331
1332
|
generatePrimaryExpr: () => "Color",
|
|
1332
1333
|
getScopeSeparator: () => "_",
|
|
@@ -1336,6 +1337,48 @@ describe("PostfixExpressionGenerator", () => {
|
|
|
1336
1337
|
generatePostfixExpression(ctx, input, state, orchestrator),
|
|
1337
1338
|
).toThrow("Use 'global.Color.Red' to access enum 'Color'");
|
|
1338
1339
|
});
|
|
1340
|
+
|
|
1341
|
+
it("allows enum access without global prefix when no naming conflict", () => {
|
|
1342
|
+
const symbols = createMockSymbols({
|
|
1343
|
+
knownEnums: new Set(["Color"]),
|
|
1344
|
+
});
|
|
1345
|
+
const ctx = createMockPostfixExpressionContext("Color", [
|
|
1346
|
+
createMockPostfixOp({ identifier: "Red" }),
|
|
1347
|
+
]);
|
|
1348
|
+
const input = createMockInput({ symbols });
|
|
1349
|
+
const scopeMembers = new Map([["Motor", new Set(["speed"])]]);
|
|
1350
|
+
const state = createMockState({ currentScope: "Motor", scopeMembers });
|
|
1351
|
+
const orchestrator = createMockOrchestrator({
|
|
1352
|
+
generatePrimaryExpr: () => "Color",
|
|
1353
|
+
getScopeSeparator: () => "_",
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
const result = generatePostfixExpression(ctx, input, state, orchestrator);
|
|
1357
|
+
expect(result.code).toBe("Color_Red");
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
it("throws when scope member shadows global enum (resolved identifier differs)", () => {
|
|
1361
|
+
const symbols = createMockSymbols({
|
|
1362
|
+
knownEnums: new Set(["Color"]),
|
|
1363
|
+
});
|
|
1364
|
+
const ctx = createMockPostfixExpressionContext("Color", [
|
|
1365
|
+
createMockPostfixOp({ identifier: "Red" }),
|
|
1366
|
+
]);
|
|
1367
|
+
const input = createMockInput({ symbols });
|
|
1368
|
+
const scopeMembers = new Map([["Motor", new Set(["Color"])]]);
|
|
1369
|
+
const state = createMockState({ currentScope: "Motor", scopeMembers });
|
|
1370
|
+
const orchestrator = createMockOrchestrator({
|
|
1371
|
+
// Simulates identifier resolution: Color -> Motor_Color (scope member)
|
|
1372
|
+
generatePrimaryExpr: () => "Motor_Color",
|
|
1373
|
+
getScopeSeparator: () => "_",
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
expect(() =>
|
|
1377
|
+
generatePostfixExpression(ctx, input, state, orchestrator),
|
|
1378
|
+
).toThrow(
|
|
1379
|
+
"Use 'global.Color.Red' to access enum 'Color' from inside scope 'Motor' (scope member 'Color' shadows the global enum)",
|
|
1380
|
+
);
|
|
1381
|
+
});
|
|
1339
1382
|
});
|
|
1340
1383
|
|
|
1341
1384
|
describe("register member access", () => {
|
|
@@ -1375,7 +1418,7 @@ describe("PostfixExpressionGenerator", () => {
|
|
|
1375
1418
|
).toThrow("cannot read from write-only register member 'DATA'");
|
|
1376
1419
|
});
|
|
1377
1420
|
|
|
1378
|
-
it("throws when accessing register
|
|
1421
|
+
it("throws when accessing register with naming conflict inside scope", () => {
|
|
1379
1422
|
const symbols = createMockSymbols({
|
|
1380
1423
|
knownRegisters: new Set(["GPIO"]),
|
|
1381
1424
|
});
|
|
@@ -1383,7 +1426,8 @@ describe("PostfixExpressionGenerator", () => {
|
|
|
1383
1426
|
createMockPostfixOp({ identifier: "PIN0" }),
|
|
1384
1427
|
]);
|
|
1385
1428
|
const input = createMockInput({ symbols });
|
|
1386
|
-
const
|
|
1429
|
+
const scopeMembers = new Map([["Motor", new Set(["GPIO"])]]);
|
|
1430
|
+
const state = createMockState({ currentScope: "Motor", scopeMembers });
|
|
1387
1431
|
const orchestrator = createMockOrchestrator({
|
|
1388
1432
|
generatePrimaryExpr: () => "GPIO",
|
|
1389
1433
|
});
|
|
@@ -1392,6 +1436,24 @@ describe("PostfixExpressionGenerator", () => {
|
|
|
1392
1436
|
generatePostfixExpression(ctx, input, state, orchestrator),
|
|
1393
1437
|
).toThrow("Use 'global.GPIO.PIN0' to access register 'GPIO'");
|
|
1394
1438
|
});
|
|
1439
|
+
|
|
1440
|
+
it("allows register access without global prefix when no naming conflict", () => {
|
|
1441
|
+
const symbols = createMockSymbols({
|
|
1442
|
+
knownRegisters: new Set(["GPIO"]),
|
|
1443
|
+
});
|
|
1444
|
+
const ctx = createMockPostfixExpressionContext("GPIO", [
|
|
1445
|
+
createMockPostfixOp({ identifier: "PIN0" }),
|
|
1446
|
+
]);
|
|
1447
|
+
const input = createMockInput({ symbols });
|
|
1448
|
+
const scopeMembers = new Map([["Motor", new Set(["speed"])]]);
|
|
1449
|
+
const state = createMockState({ currentScope: "Motor", scopeMembers });
|
|
1450
|
+
const orchestrator = createMockOrchestrator({
|
|
1451
|
+
generatePrimaryExpr: () => "GPIO",
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
const result = generatePostfixExpression(ctx, input, state, orchestrator);
|
|
1455
|
+
expect(result.code).toBe("GPIO_PIN0");
|
|
1456
|
+
});
|
|
1395
1457
|
});
|
|
1396
1458
|
|
|
1397
1459
|
describe("struct parameter access", () => {
|
|
@@ -55,13 +55,22 @@ class MemberAccessValidator {
|
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* ADR-016/057: Validate that a global entity (enum or register) is accessed
|
|
58
|
-
* with 'global.' prefix when inside a scope that
|
|
58
|
+
* with 'global.' prefix when inside a scope that has a naming conflict.
|
|
59
59
|
*
|
|
60
|
-
*
|
|
60
|
+
* Detects two types of conflicts:
|
|
61
|
+
* 1. Scope member with same name as entity (via scopeMembers lookup)
|
|
62
|
+
* 2. Identifier was resolved to scope member, shadowing a global enum
|
|
63
|
+
* (via rootIdentifier != resolvedName comparison)
|
|
64
|
+
*
|
|
65
|
+
* @param entityName - The resolved entity name (e.g., "Color" or "Motor_Color")
|
|
61
66
|
* @param memberName - The member after the entity (e.g., "Red", "PIN0")
|
|
62
67
|
* @param entityType - "enum" or "register" (for error message)
|
|
63
68
|
* @param currentScope - Active scope context (null = not in a scope)
|
|
64
69
|
* @param isGlobalAccess - Whether the access used 'global.' prefix
|
|
70
|
+
* @param options - Optional conflict detection parameters
|
|
71
|
+
* @param options.scopeMembers - Map of scope names to their member names
|
|
72
|
+
* @param options.rootIdentifier - Original identifier before resolution (for shadowing detection)
|
|
73
|
+
* @param options.knownEnums - Set of known enum names (for shadowing detection)
|
|
65
74
|
*/
|
|
66
75
|
static validateGlobalEntityAccess(
|
|
67
76
|
entityName: string,
|
|
@@ -69,18 +78,50 @@ class MemberAccessValidator {
|
|
|
69
78
|
entityType: string,
|
|
70
79
|
currentScope: string | null,
|
|
71
80
|
isGlobalAccess: boolean,
|
|
81
|
+
options?: {
|
|
82
|
+
scopeMembers?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
83
|
+
rootIdentifier?: string;
|
|
84
|
+
knownEnums?: ReadonlySet<string>;
|
|
85
|
+
},
|
|
72
86
|
): void {
|
|
73
87
|
if (isGlobalAccess) {
|
|
74
88
|
return;
|
|
75
89
|
}
|
|
76
|
-
if (currentScope) {
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
if (!currentScope) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { scopeMembers, rootIdentifier, knownEnums } = options ?? {};
|
|
95
|
+
|
|
96
|
+
// Check 1: Shadowing detection - identifier was resolved to a scope member
|
|
97
|
+
// that shadows a global enum. This produces invalid C (e.g., Motor_Color.RED).
|
|
98
|
+
// This check must run BEFORE the belongsToCurrentScope check because
|
|
99
|
+
// the resolved name (e.g., Motor_Color) will start with the scope prefix.
|
|
100
|
+
if (rootIdentifier && knownEnums) {
|
|
101
|
+
const wasResolved = entityName !== rootIdentifier;
|
|
102
|
+
const rootIsEnum = knownEnums.has(rootIdentifier);
|
|
103
|
+
const resolvedIsNotEnum = !knownEnums.has(entityName);
|
|
104
|
+
if (wasResolved && rootIsEnum && resolvedIsNotEnum) {
|
|
79
105
|
throw new Error(
|
|
80
|
-
`Error: Use 'global.${
|
|
106
|
+
`Error: Use 'global.${rootIdentifier}.${memberName}' to access enum '${rootIdentifier}' from inside scope '${currentScope}' (scope member '${rootIdentifier}' shadows the global enum)`,
|
|
81
107
|
);
|
|
82
108
|
}
|
|
83
109
|
}
|
|
110
|
+
|
|
111
|
+
// Skip check if entity belongs to current scope (e.g., Motor_State in scope Motor)
|
|
112
|
+
const belongsToCurrentScope = entityName.startsWith(currentScope + "_");
|
|
113
|
+
if (belongsToCurrentScope) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check 2: Direct conflict - scope has a member with the same name as the entity
|
|
118
|
+
const scopeMemberNames = scopeMembers?.get(currentScope);
|
|
119
|
+
const hasConflict = scopeMemberNames?.has(entityName) ?? false;
|
|
120
|
+
if (hasConflict) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Error: Use 'global.${entityName}.${memberName}' to access ${entityType} '${entityName}' from inside scope '${currentScope}'`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
84
125
|
}
|
|
85
126
|
}
|
|
86
127
|
|
|
@@ -118,7 +118,8 @@ describe("MemberAccessValidator", () => {
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
describe("validateGlobalEntityAccess", () => {
|
|
121
|
-
it("throws when in scope and entity
|
|
121
|
+
it("throws when in scope and entity name conflicts with scope member", () => {
|
|
122
|
+
const scopeMembers = new Map([["Motor", new Set(["Color", "speed"])]]);
|
|
122
123
|
expect(() =>
|
|
123
124
|
MemberAccessValidator.validateGlobalEntityAccess(
|
|
124
125
|
"Color",
|
|
@@ -126,13 +127,29 @@ describe("MemberAccessValidator", () => {
|
|
|
126
127
|
"enum",
|
|
127
128
|
"Motor",
|
|
128
129
|
false,
|
|
130
|
+
{ scopeMembers },
|
|
129
131
|
),
|
|
130
132
|
).toThrow(
|
|
131
133
|
"Use 'global.Color.Red' to access enum 'Color' from inside scope 'Motor'",
|
|
132
134
|
);
|
|
133
135
|
});
|
|
134
136
|
|
|
135
|
-
it("does NOT throw when
|
|
137
|
+
it("does NOT throw when in scope but no naming conflict", () => {
|
|
138
|
+
const scopeMembers = new Map([["Motor", new Set(["speed"])]]);
|
|
139
|
+
expect(() =>
|
|
140
|
+
MemberAccessValidator.validateGlobalEntityAccess(
|
|
141
|
+
"Color",
|
|
142
|
+
"Red",
|
|
143
|
+
"enum",
|
|
144
|
+
"Motor",
|
|
145
|
+
false,
|
|
146
|
+
{ scopeMembers },
|
|
147
|
+
),
|
|
148
|
+
).not.toThrow();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("does NOT throw when isGlobalAccess is true even with conflict", () => {
|
|
152
|
+
const scopeMembers = new Map([["Motor", new Set(["Color"])]]);
|
|
136
153
|
expect(() =>
|
|
137
154
|
MemberAccessValidator.validateGlobalEntityAccess(
|
|
138
155
|
"Color",
|
|
@@ -140,6 +157,7 @@ describe("MemberAccessValidator", () => {
|
|
|
140
157
|
"enum",
|
|
141
158
|
"Motor",
|
|
142
159
|
true,
|
|
160
|
+
{ scopeMembers },
|
|
143
161
|
),
|
|
144
162
|
).not.toThrow();
|
|
145
163
|
});
|
|
@@ -168,7 +186,8 @@ describe("MemberAccessValidator", () => {
|
|
|
168
186
|
).not.toThrow();
|
|
169
187
|
});
|
|
170
188
|
|
|
171
|
-
it("
|
|
189
|
+
it("throws for register with naming conflict", () => {
|
|
190
|
+
const scopeMembers = new Map([["Motor", new Set(["GPIO"])]]);
|
|
172
191
|
expect(() =>
|
|
173
192
|
MemberAccessValidator.validateGlobalEntityAccess(
|
|
174
193
|
"GPIO",
|
|
@@ -176,10 +195,97 @@ describe("MemberAccessValidator", () => {
|
|
|
176
195
|
"register",
|
|
177
196
|
"Motor",
|
|
178
197
|
false,
|
|
198
|
+
{ scopeMembers },
|
|
179
199
|
),
|
|
180
200
|
).toThrow(
|
|
181
201
|
"Use 'global.GPIO.PIN0' to access register 'GPIO' from inside scope 'Motor'",
|
|
182
202
|
);
|
|
183
203
|
});
|
|
204
|
+
|
|
205
|
+
it("does NOT throw for register without naming conflict", () => {
|
|
206
|
+
const scopeMembers = new Map([["Motor", new Set(["speed"])]]);
|
|
207
|
+
expect(() =>
|
|
208
|
+
MemberAccessValidator.validateGlobalEntityAccess(
|
|
209
|
+
"GPIO",
|
|
210
|
+
"PIN0",
|
|
211
|
+
"register",
|
|
212
|
+
"Motor",
|
|
213
|
+
false,
|
|
214
|
+
{ scopeMembers },
|
|
215
|
+
),
|
|
216
|
+
).not.toThrow();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("does NOT throw when options is undefined", () => {
|
|
220
|
+
expect(() =>
|
|
221
|
+
MemberAccessValidator.validateGlobalEntityAccess(
|
|
222
|
+
"Color",
|
|
223
|
+
"Red",
|
|
224
|
+
"enum",
|
|
225
|
+
"Motor",
|
|
226
|
+
false,
|
|
227
|
+
undefined,
|
|
228
|
+
),
|
|
229
|
+
).not.toThrow();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Shadowing detection tests (using rootIdentifier and knownEnums)
|
|
233
|
+
it("throws when scope member shadows global enum (resolved identifier differs)", () => {
|
|
234
|
+
const knownEnums = new Set(["Color"]);
|
|
235
|
+
expect(() =>
|
|
236
|
+
MemberAccessValidator.validateGlobalEntityAccess(
|
|
237
|
+
"Motor_Color", // resolved to scope member
|
|
238
|
+
"Red",
|
|
239
|
+
"enum",
|
|
240
|
+
"Motor",
|
|
241
|
+
false,
|
|
242
|
+
{ rootIdentifier: "Color", knownEnums },
|
|
243
|
+
),
|
|
244
|
+
).toThrow(
|
|
245
|
+
"Use 'global.Color.Red' to access enum 'Color' from inside scope 'Motor' (scope member 'Color' shadows the global enum)",
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("does NOT throw for shadowing when isGlobalAccess is true", () => {
|
|
250
|
+
const knownEnums = new Set(["Color"]);
|
|
251
|
+
expect(() =>
|
|
252
|
+
MemberAccessValidator.validateGlobalEntityAccess(
|
|
253
|
+
"Motor_Color",
|
|
254
|
+
"Red",
|
|
255
|
+
"enum",
|
|
256
|
+
"Motor",
|
|
257
|
+
true,
|
|
258
|
+
{ rootIdentifier: "Color", knownEnums },
|
|
259
|
+
),
|
|
260
|
+
).not.toThrow();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("does NOT throw when root is not a known enum", () => {
|
|
264
|
+
const knownEnums = new Set(["OtherEnum"]);
|
|
265
|
+
expect(() =>
|
|
266
|
+
MemberAccessValidator.validateGlobalEntityAccess(
|
|
267
|
+
"Motor_Color",
|
|
268
|
+
"Red",
|
|
269
|
+
"enum",
|
|
270
|
+
"Motor",
|
|
271
|
+
false,
|
|
272
|
+
{ rootIdentifier: "Color", knownEnums },
|
|
273
|
+
),
|
|
274
|
+
).not.toThrow();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("does NOT throw when resolved name IS a known enum", () => {
|
|
278
|
+
const knownEnums = new Set(["Color", "Motor_Color"]);
|
|
279
|
+
expect(() =>
|
|
280
|
+
MemberAccessValidator.validateGlobalEntityAccess(
|
|
281
|
+
"Motor_Color",
|
|
282
|
+
"Red",
|
|
283
|
+
"enum",
|
|
284
|
+
"Motor",
|
|
285
|
+
false,
|
|
286
|
+
{ rootIdentifier: "Color", knownEnums },
|
|
287
|
+
),
|
|
288
|
+
).not.toThrow();
|
|
289
|
+
});
|
|
184
290
|
});
|
|
185
291
|
});
|
|
@@ -90,6 +90,13 @@ export default class CodeGenState {
|
|
|
90
90
|
*/
|
|
91
91
|
static symbolTable: SymbolTable = new SymbolTable();
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* External struct fields from C/C++ headers for initialization analysis.
|
|
95
|
+
* Maps struct name -> Set of non-array field names.
|
|
96
|
+
* Persists across per-file reset() calls, cleared at start of run.
|
|
97
|
+
*/
|
|
98
|
+
static externalStructFields: Map<string, Set<string>> = new Map();
|
|
99
|
+
|
|
93
100
|
// ===========================================================================
|
|
94
101
|
// TYPE TRACKING
|
|
95
102
|
// ===========================================================================
|
|
@@ -650,6 +657,39 @@ export default class CodeGenState {
|
|
|
650
657
|
return this.symbols?.enumMembers.get(enumName);
|
|
651
658
|
}
|
|
652
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Build external struct fields from the symbol table.
|
|
662
|
+
* Called once per run after all headers are processed.
|
|
663
|
+
* Issue #355: Excludes array fields from init checking.
|
|
664
|
+
*/
|
|
665
|
+
static buildExternalStructFields(): void {
|
|
666
|
+
this.externalStructFields.clear();
|
|
667
|
+
const allStructFields = this.symbolTable.getAllStructFields();
|
|
668
|
+
|
|
669
|
+
for (const [structName, fieldMap] of allStructFields) {
|
|
670
|
+
const nonArrayFields = new Set<string>();
|
|
671
|
+
for (const [fieldName, fieldInfo] of fieldMap) {
|
|
672
|
+
// Only include non-array fields in init checking
|
|
673
|
+
if (
|
|
674
|
+
!fieldInfo.arrayDimensions ||
|
|
675
|
+
fieldInfo.arrayDimensions.length === 0
|
|
676
|
+
) {
|
|
677
|
+
nonArrayFields.add(fieldName);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (nonArrayFields.size > 0) {
|
|
681
|
+
this.externalStructFields.set(structName, nonArrayFields);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Get external struct fields for initialization analysis.
|
|
688
|
+
*/
|
|
689
|
+
static getExternalStructFields(): Map<string, Set<string>> {
|
|
690
|
+
return this.externalStructFields;
|
|
691
|
+
}
|
|
692
|
+
|
|
653
693
|
/**
|
|
654
694
|
* Get function return type.
|
|
655
695
|
*/
|
|
@@ -453,4 +453,123 @@ describe("CodeGenState", () => {
|
|
|
453
453
|
expect(CodeGenState.isLocalArray("myArr")).toBe(true);
|
|
454
454
|
});
|
|
455
455
|
});
|
|
456
|
+
|
|
457
|
+
describe("buildExternalStructFields", () => {
|
|
458
|
+
it("returns empty map when no struct fields exist", () => {
|
|
459
|
+
CodeGenState.buildExternalStructFields();
|
|
460
|
+
const result = CodeGenState.getExternalStructFields();
|
|
461
|
+
expect(result.size).toBe(0);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("includes non-array fields in result", () => {
|
|
465
|
+
// Manually add struct fields to the symbol table
|
|
466
|
+
const structFields = new Map<
|
|
467
|
+
string,
|
|
468
|
+
Map<string, { type: string; arrayDimensions?: number[] }>
|
|
469
|
+
>();
|
|
470
|
+
const pointFields = new Map<
|
|
471
|
+
string,
|
|
472
|
+
{ type: string; arrayDimensions?: number[] }
|
|
473
|
+
>();
|
|
474
|
+
pointFields.set("x", { type: "i32" });
|
|
475
|
+
pointFields.set("y", { type: "i32" });
|
|
476
|
+
structFields.set("Point", pointFields);
|
|
477
|
+
|
|
478
|
+
// Use restoreStructFields to populate the symbol table
|
|
479
|
+
CodeGenState.symbolTable.restoreStructFields(structFields);
|
|
480
|
+
|
|
481
|
+
CodeGenState.buildExternalStructFields();
|
|
482
|
+
const result = CodeGenState.getExternalStructFields();
|
|
483
|
+
|
|
484
|
+
expect(result.has("Point")).toBe(true);
|
|
485
|
+
const fields = result.get("Point");
|
|
486
|
+
expect(fields?.has("x")).toBe(true);
|
|
487
|
+
expect(fields?.has("y")).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("excludes array fields from result (Issue #355)", () => {
|
|
491
|
+
const structFields = new Map<
|
|
492
|
+
string,
|
|
493
|
+
Map<string, { type: string; arrayDimensions?: number[] }>
|
|
494
|
+
>();
|
|
495
|
+
const bufferFields = new Map<
|
|
496
|
+
string,
|
|
497
|
+
{ type: string; arrayDimensions?: number[] }
|
|
498
|
+
>();
|
|
499
|
+
bufferFields.set("size", { type: "u32" }); // Non-array
|
|
500
|
+
bufferFields.set("data", { type: "u8", arrayDimensions: [256] }); // Array
|
|
501
|
+
|
|
502
|
+
structFields.set("Buffer", bufferFields);
|
|
503
|
+
CodeGenState.symbolTable.restoreStructFields(structFields);
|
|
504
|
+
|
|
505
|
+
CodeGenState.buildExternalStructFields();
|
|
506
|
+
const result = CodeGenState.getExternalStructFields();
|
|
507
|
+
|
|
508
|
+
expect(result.has("Buffer")).toBe(true);
|
|
509
|
+
const fields = result.get("Buffer");
|
|
510
|
+
expect(fields?.has("size")).toBe(true); // Non-array included
|
|
511
|
+
expect(fields?.has("data")).toBe(false); // Array excluded
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("excludes structs with only array fields", () => {
|
|
515
|
+
const structFields = new Map<
|
|
516
|
+
string,
|
|
517
|
+
Map<string, { type: string; arrayDimensions?: number[] }>
|
|
518
|
+
>();
|
|
519
|
+
const arrayOnlyFields = new Map<
|
|
520
|
+
string,
|
|
521
|
+
{ type: string; arrayDimensions?: number[] }
|
|
522
|
+
>();
|
|
523
|
+
arrayOnlyFields.set("items", { type: "u8", arrayDimensions: [10] });
|
|
524
|
+
arrayOnlyFields.set("values", { type: "i32", arrayDimensions: [5] });
|
|
525
|
+
|
|
526
|
+
structFields.set("ArrayOnly", arrayOnlyFields);
|
|
527
|
+
CodeGenState.symbolTable.restoreStructFields(structFields);
|
|
528
|
+
|
|
529
|
+
CodeGenState.buildExternalStructFields();
|
|
530
|
+
const result = CodeGenState.getExternalStructFields();
|
|
531
|
+
|
|
532
|
+
// Struct should not be included since all fields are arrays
|
|
533
|
+
expect(result.has("ArrayOnly")).toBe(false);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("handles mixed structs correctly", () => {
|
|
537
|
+
const structFields = new Map<
|
|
538
|
+
string,
|
|
539
|
+
Map<string, { type: string; arrayDimensions?: number[] }>
|
|
540
|
+
>();
|
|
541
|
+
|
|
542
|
+
// Struct with mixed fields
|
|
543
|
+
const mixedFields = new Map<
|
|
544
|
+
string,
|
|
545
|
+
{ type: string; arrayDimensions?: number[] }
|
|
546
|
+
>();
|
|
547
|
+
mixedFields.set("id", { type: "u32" });
|
|
548
|
+
mixedFields.set("name", { type: "string", arrayDimensions: [32] });
|
|
549
|
+
mixedFields.set("count", { type: "u16" });
|
|
550
|
+
|
|
551
|
+
// Struct with only non-array
|
|
552
|
+
const simpleFields = new Map<
|
|
553
|
+
string,
|
|
554
|
+
{ type: string; arrayDimensions?: number[] }
|
|
555
|
+
>();
|
|
556
|
+
simpleFields.set("value", { type: "f32" });
|
|
557
|
+
|
|
558
|
+
structFields.set("Mixed", mixedFields);
|
|
559
|
+
structFields.set("Simple", simpleFields);
|
|
560
|
+
CodeGenState.symbolTable.restoreStructFields(structFields);
|
|
561
|
+
|
|
562
|
+
CodeGenState.buildExternalStructFields();
|
|
563
|
+
const result = CodeGenState.getExternalStructFields();
|
|
564
|
+
|
|
565
|
+
// Mixed struct should have only non-array fields
|
|
566
|
+
expect(result.get("Mixed")?.size).toBe(2);
|
|
567
|
+
expect(result.get("Mixed")?.has("id")).toBe(true);
|
|
568
|
+
expect(result.get("Mixed")?.has("count")).toBe(true);
|
|
569
|
+
expect(result.get("Mixed")?.has("name")).toBe(false);
|
|
570
|
+
|
|
571
|
+
// Simple struct should have its field
|
|
572
|
+
expect(result.get("Simple")?.has("value")).toBe(true);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
456
575
|
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AnalyzerContextBuilder
|
|
3
|
-
* Issue #591: Extracted from Transpiler.transpileSource() to reduce complexity
|
|
4
|
-
*
|
|
5
|
-
* Builds the context needed for running semantic analyzers, specifically
|
|
6
|
-
* converting struct field information from the symbol table format to
|
|
7
|
-
* the analyzer-compatible format.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import SymbolTable from "../symbols/SymbolTable";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Builds analyzer context from symbol table data.
|
|
14
|
-
*
|
|
15
|
-
* The analyzer requires struct fields as Map<string, Set<string>> (structName -> fieldNames)
|
|
16
|
-
* while the symbol table stores more detailed IStructFieldInfo. This builder handles
|
|
17
|
-
* the conversion, including Issue #355's requirement to exclude array fields.
|
|
18
|
-
*/
|
|
19
|
-
class AnalyzerContextBuilder {
|
|
20
|
-
/**
|
|
21
|
-
* Build external struct fields map for analyzer from symbol table.
|
|
22
|
-
*
|
|
23
|
-
* Converts the symbol table's detailed struct field info to the simpler
|
|
24
|
-
* format needed by InitializationAnalyzer.
|
|
25
|
-
*
|
|
26
|
-
* Issue #355: Excludes array fields from init checking since the analyzer
|
|
27
|
-
* can't prove loop-based array initialization is complete.
|
|
28
|
-
*
|
|
29
|
-
* @param symbolTable - Symbol table containing struct definitions
|
|
30
|
-
* @returns Map of struct name to set of non-array field names
|
|
31
|
-
*/
|
|
32
|
-
static buildExternalStructFields(
|
|
33
|
-
symbolTable: SymbolTable,
|
|
34
|
-
): Map<string, Set<string>> {
|
|
35
|
-
const externalStructFields = new Map<string, Set<string>>();
|
|
36
|
-
const allStructFields = symbolTable.getAllStructFields();
|
|
37
|
-
|
|
38
|
-
for (const [structName, fieldMap] of allStructFields) {
|
|
39
|
-
const nonArrayFields = new Set<string>();
|
|
40
|
-
for (const [fieldName, fieldInfo] of fieldMap) {
|
|
41
|
-
// Only include non-array fields in init checking
|
|
42
|
-
if (
|
|
43
|
-
!fieldInfo.arrayDimensions ||
|
|
44
|
-
fieldInfo.arrayDimensions.length === 0
|
|
45
|
-
) {
|
|
46
|
-
nonArrayFields.add(fieldName);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (nonArrayFields.size > 0) {
|
|
50
|
-
externalStructFields.set(structName, nonArrayFields);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return externalStructFields;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export default AnalyzerContextBuilder;
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for AnalyzerContextBuilder
|
|
3
|
-
* Issue #591: Tests for struct field conversion utility
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
7
|
-
|
|
8
|
-
import AnalyzerContextBuilder from "../AnalyzerContextBuilder";
|
|
9
|
-
import SymbolTable from "../../symbols/SymbolTable";
|
|
10
|
-
|
|
11
|
-
describe("AnalyzerContextBuilder", () => {
|
|
12
|
-
let symbolTable: SymbolTable;
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
symbolTable = new SymbolTable();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("buildExternalStructFields", () => {
|
|
19
|
-
it("returns empty map when no struct fields exist", () => {
|
|
20
|
-
const result =
|
|
21
|
-
AnalyzerContextBuilder.buildExternalStructFields(symbolTable);
|
|
22
|
-
|
|
23
|
-
expect(result.size).toBe(0);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("includes non-array fields in result", () => {
|
|
27
|
-
// Manually add struct fields to the symbol table
|
|
28
|
-
const structFields = new Map<
|
|
29
|
-
string,
|
|
30
|
-
Map<string, { type: string; arrayDimensions?: number[] }>
|
|
31
|
-
>();
|
|
32
|
-
const pointFields = new Map<
|
|
33
|
-
string,
|
|
34
|
-
{ type: string; arrayDimensions?: number[] }
|
|
35
|
-
>();
|
|
36
|
-
pointFields.set("x", { type: "i32" });
|
|
37
|
-
pointFields.set("y", { type: "i32" });
|
|
38
|
-
structFields.set("Point", pointFields);
|
|
39
|
-
|
|
40
|
-
// Use restoreStructFields to populate the symbol table
|
|
41
|
-
symbolTable.restoreStructFields(structFields);
|
|
42
|
-
|
|
43
|
-
const result =
|
|
44
|
-
AnalyzerContextBuilder.buildExternalStructFields(symbolTable);
|
|
45
|
-
|
|
46
|
-
expect(result.has("Point")).toBe(true);
|
|
47
|
-
const fields = result.get("Point");
|
|
48
|
-
expect(fields?.has("x")).toBe(true);
|
|
49
|
-
expect(fields?.has("y")).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("excludes array fields from result (Issue #355)", () => {
|
|
53
|
-
const structFields = new Map<
|
|
54
|
-
string,
|
|
55
|
-
Map<string, { type: string; arrayDimensions?: number[] }>
|
|
56
|
-
>();
|
|
57
|
-
const bufferFields = new Map<
|
|
58
|
-
string,
|
|
59
|
-
{ type: string; arrayDimensions?: number[] }
|
|
60
|
-
>();
|
|
61
|
-
bufferFields.set("size", { type: "u32" }); // Non-array
|
|
62
|
-
bufferFields.set("data", { type: "u8", arrayDimensions: [256] }); // Array
|
|
63
|
-
|
|
64
|
-
structFields.set("Buffer", bufferFields);
|
|
65
|
-
symbolTable.restoreStructFields(structFields);
|
|
66
|
-
|
|
67
|
-
const result =
|
|
68
|
-
AnalyzerContextBuilder.buildExternalStructFields(symbolTable);
|
|
69
|
-
|
|
70
|
-
expect(result.has("Buffer")).toBe(true);
|
|
71
|
-
const fields = result.get("Buffer");
|
|
72
|
-
expect(fields?.has("size")).toBe(true); // Non-array included
|
|
73
|
-
expect(fields?.has("data")).toBe(false); // Array excluded
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("excludes structs with only array fields", () => {
|
|
77
|
-
const structFields = new Map<
|
|
78
|
-
string,
|
|
79
|
-
Map<string, { type: string; arrayDimensions?: number[] }>
|
|
80
|
-
>();
|
|
81
|
-
const arrayOnlyFields = new Map<
|
|
82
|
-
string,
|
|
83
|
-
{ type: string; arrayDimensions?: number[] }
|
|
84
|
-
>();
|
|
85
|
-
arrayOnlyFields.set("items", { type: "u8", arrayDimensions: [10] });
|
|
86
|
-
arrayOnlyFields.set("values", { type: "i32", arrayDimensions: [5] });
|
|
87
|
-
|
|
88
|
-
structFields.set("ArrayOnly", arrayOnlyFields);
|
|
89
|
-
symbolTable.restoreStructFields(structFields);
|
|
90
|
-
|
|
91
|
-
const result =
|
|
92
|
-
AnalyzerContextBuilder.buildExternalStructFields(symbolTable);
|
|
93
|
-
|
|
94
|
-
// Struct should not be included since all fields are arrays
|
|
95
|
-
expect(result.has("ArrayOnly")).toBe(false);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("handles mixed structs correctly", () => {
|
|
99
|
-
const structFields = new Map<
|
|
100
|
-
string,
|
|
101
|
-
Map<string, { type: string; arrayDimensions?: number[] }>
|
|
102
|
-
>();
|
|
103
|
-
|
|
104
|
-
// Struct with mixed fields
|
|
105
|
-
const mixedFields = new Map<
|
|
106
|
-
string,
|
|
107
|
-
{ type: string; arrayDimensions?: number[] }
|
|
108
|
-
>();
|
|
109
|
-
mixedFields.set("id", { type: "u32" });
|
|
110
|
-
mixedFields.set("name", { type: "string", arrayDimensions: [32] });
|
|
111
|
-
mixedFields.set("count", { type: "u16" });
|
|
112
|
-
|
|
113
|
-
// Struct with only non-array
|
|
114
|
-
const simpleFields = new Map<
|
|
115
|
-
string,
|
|
116
|
-
{ type: string; arrayDimensions?: number[] }
|
|
117
|
-
>();
|
|
118
|
-
simpleFields.set("value", { type: "f32" });
|
|
119
|
-
|
|
120
|
-
structFields.set("Mixed", mixedFields);
|
|
121
|
-
structFields.set("Simple", simpleFields);
|
|
122
|
-
symbolTable.restoreStructFields(structFields);
|
|
123
|
-
|
|
124
|
-
const result =
|
|
125
|
-
AnalyzerContextBuilder.buildExternalStructFields(symbolTable);
|
|
126
|
-
|
|
127
|
-
// Mixed struct should have only non-array fields
|
|
128
|
-
expect(result.get("Mixed")?.size).toBe(2);
|
|
129
|
-
expect(result.get("Mixed")?.has("id")).toBe(true);
|
|
130
|
-
expect(result.get("Mixed")?.has("count")).toBe(true);
|
|
131
|
-
expect(result.get("Mixed")?.has("name")).toBe(false);
|
|
132
|
-
|
|
133
|
-
// Simple struct should have its field
|
|
134
|
-
expect(result.get("Simple")?.has("value")).toBe(true);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
});
|