c-next 0.2.0 → 0.2.2

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 (37) hide show
  1. package/bin/cnext.js +17 -7
  2. package/dist/index.js +139747 -0
  3. package/dist/index.js.map +7 -0
  4. package/package.json +8 -4
  5. package/src/cli/Cli.ts +5 -2
  6. package/src/cli/PathNormalizer.ts +170 -0
  7. package/src/cli/PlatformIOCommand.ts +51 -3
  8. package/src/cli/__tests__/Cli.integration.test.ts +100 -0
  9. package/src/cli/__tests__/Cli.test.ts +17 -12
  10. package/src/cli/__tests__/PathNormalizer.test.ts +411 -0
  11. package/src/cli/__tests__/PlatformIOCommand.test.ts +156 -0
  12. package/src/cli/serve/__tests__/ServeCommand.test.ts +1 -1
  13. package/src/lib/__tests__/parseWithSymbols.test.ts +228 -0
  14. package/src/lib/parseCHeader.ts +5 -1
  15. package/src/lib/parseWithSymbols.ts +62 -5
  16. package/src/lib/types/ISymbolInfo.ts +4 -0
  17. package/src/lib/utils/SymbolPathUtils.ts +87 -0
  18. package/src/lib/utils/__tests__/SymbolPathUtils.test.ts +123 -0
  19. package/src/transpiler/NodeFileSystem.ts +5 -0
  20. package/src/transpiler/logic/symbols/SymbolTable.ts +17 -0
  21. package/src/transpiler/output/codegen/CodeGenerator.ts +27 -32
  22. package/src/transpiler/output/codegen/TypeResolver.ts +12 -24
  23. package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +130 -5
  24. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +67 -57
  25. package/src/transpiler/output/codegen/analysis/MemberChainAnalyzer.ts +9 -13
  26. package/src/transpiler/output/codegen/analysis/__tests__/MemberChainAnalyzer.test.ts +20 -10
  27. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +5 -2
  28. package/src/transpiler/output/codegen/assignment/__tests__/AssignmentClassifier.test.ts +18 -0
  29. package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +25 -4
  30. package/src/transpiler/output/codegen/assignment/handlers/__tests__/handlerTestUtils.ts +18 -0
  31. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +51 -2
  32. package/src/transpiler/output/codegen/generators/expressions/LiteralGenerator.ts +76 -8
  33. package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +147 -0
  34. package/src/transpiler/output/codegen/generators/expressions/__tests__/LiteralGenerator.test.ts +116 -0
  35. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +6 -5
  36. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +14 -5
  37. package/src/transpiler/types/IFileSystem.ts +8 -0
@@ -18,6 +18,7 @@ import IGeneratorState from "../IGeneratorState";
18
18
  import IOrchestrator from "../IOrchestrator";
19
19
  import CallExprUtils from "./CallExprUtils";
20
20
  import CodeGenState from "../../../../state/CodeGenState";
21
+ import C_TYPE_WIDTH from "../../types/C_TYPE_WIDTH";
21
22
 
22
23
  /**
23
24
  * Issue #304: Wrap argument with static_cast if it's a C++ enum class
@@ -58,6 +59,51 @@ interface IResolvedParam {
58
59
  isCrossFile: boolean;
59
60
  }
60
61
 
62
+ /**
63
+ * Issue #832: Check if parameter expects address-of for typedef'd pointer types.
64
+ *
65
+ * When a parameter type is `T*` and the argument type is `T`, we need to add `&`.
66
+ * This handles cases like `handle_t` (typedef'd pointer) passed to `handle_t*`.
67
+ *
68
+ * IMPORTANT: This should NOT match primitive types like uint8_t, because arrays
69
+ * of primitives decay to pointers naturally (uint8_t[] → uint8_t*).
70
+ * It SHOULD match typedef'd pointer types like handle_t → handle_t*.
71
+ *
72
+ * @param paramType - The parameter's base type (e.g., "handle_t*")
73
+ * @param argType - The argument's type (e.g., "handle_t")
74
+ * @param orchestrator - For type checking (isIntegerType, isFloatType)
75
+ * @returns true if parameter expects `argType*` (address-of needed)
76
+ */
77
+ const _parameterExpectsAddressOf = (
78
+ paramType: string,
79
+ argType: string,
80
+ orchestrator: IOrchestrator,
81
+ ): boolean => {
82
+ // Don't add & for primitive types - arrays decay to pointers naturally
83
+ // e.g., uint8_t[] passed to uint8_t* should NOT get &
84
+ // Check C-Next primitives (u8, i8, etc.)
85
+ if (
86
+ orchestrator.isIntegerType(argType) ||
87
+ orchestrator.isFloatType(argType) ||
88
+ CallExprUtils.isKnownPrimitiveType(argType)
89
+ ) {
90
+ return false;
91
+ }
92
+
93
+ // Check C standard types (uint8_t, int32_t, etc.)
94
+ if (argType in C_TYPE_WIDTH) {
95
+ return false;
96
+ }
97
+
98
+ // paramType should end with * (already checked by caller)
99
+ // Remove trailing pointer markers to get the base type
100
+ // Use indexOf/slice instead of regex to avoid ReDoS concerns (SonarCloud S5852)
101
+ const starIndex = paramType.indexOf("*");
102
+ const paramBaseType =
103
+ starIndex >= 0 ? paramType.slice(0, starIndex).trim() : paramType.trim();
104
+ return paramBaseType === argType;
105
+ };
106
+
61
107
  /**
62
108
  * Generate argument code for a C/C++ function call.
63
109
  * Handles automatic address-of (&) for struct arguments passed to pointer params.
@@ -87,13 +133,16 @@ const _generateCFunctionArg = (
87
133
  }
88
134
  }
89
135
 
90
- // Add & if argument is a struct type (not already a pointer)
136
+ // Add & if argument needs address-of to match parameter type.
137
+ // Issue #322: struct types passed to pointer params.
138
+ // Issue #832: typedef'd pointer types (e.g., handle_t passed to handle_t*).
91
139
  const needsAddressOf =
92
140
  argType &&
93
141
  !argType.endsWith("*") &&
94
142
  !argCode.startsWith("&") &&
95
143
  !targetParam.isArray &&
96
- orchestrator.isStructType(argType);
144
+ (orchestrator.isStructType(argType) ||
145
+ _parameterExpectsAddressOf(targetParam.baseType, argType, orchestrator));
97
146
 
98
147
  if (needsAddressOf) {
99
148
  argCode = `&${argCode}`;
@@ -5,6 +5,7 @@
5
5
  * - Boolean literals (true/false) → needs stdbool.h
6
6
  * - Float literals with C-Next suffixes (f32 → f, f64 → no suffix)
7
7
  * - Integer literals with C-Next suffixes (u64 → ULL, i64 → LL, strip 8/16/32)
8
+ * - MISRA Rule 7.2: Unsigned suffix for unsigned integer types
8
9
  * - String and numeric literals pass through unchanged
9
10
  */
10
11
  import { LiteralContext } from "../../../../logic/parser/grammar/CNextParser";
@@ -14,19 +15,63 @@ import IGeneratorInput from "../IGeneratorInput";
14
15
  import IGeneratorState from "../IGeneratorState";
15
16
  import IOrchestrator from "../IOrchestrator";
16
17
 
18
+ /**
19
+ * Unsigned type patterns for MISRA Rule 7.2 compliance.
20
+ * Includes both C-Next types (u8, u16, u32, u64) and C types (uint8_t, etc.)
21
+ */
22
+ const UNSIGNED_64_TYPES = new Set(["u64", "uint64_t"]);
23
+ const UNSIGNED_TYPES = new Set([
24
+ "u8",
25
+ "u16",
26
+ "u32",
27
+ "uint8_t",
28
+ "uint16_t",
29
+ "uint32_t",
30
+ ]);
31
+
32
+ /**
33
+ * Check if a literal is a numeric integer (decimal, hex, octal, or binary).
34
+ * Excludes strings, floats, and booleans.
35
+ */
36
+ function isNumericIntegerLiteral(text: string): boolean {
37
+ // Exclude strings (quoted)
38
+ if (text.startsWith('"') || text.startsWith("'")) {
39
+ return false;
40
+ }
41
+ // Exclude booleans
42
+ if (text === "true" || text === "false") {
43
+ return false;
44
+ }
45
+ // Exclude floats (contain decimal point or exponent without 0x prefix)
46
+ if (!text.startsWith("0x") && !text.startsWith("0X")) {
47
+ if (text.includes(".") || /[eE][+-]?\d/.test(text)) {
48
+ return false;
49
+ }
50
+ }
51
+ // Must start with digit or be hex/binary/octal
52
+ return /^\d/.test(text) || /^0[xXbBoO]/.test(text);
53
+ }
54
+
55
+ /**
56
+ * Check if a literal already has a C unsigned suffix (U, UL, ULL).
57
+ */
58
+ function hasUnsignedSuffix(text: string): boolean {
59
+ return /[uU]([lL]{0,2})$/.test(text);
60
+ }
61
+
17
62
  /**
18
63
  * Generate C code for a literal value.
19
64
  *
20
65
  * @param node - The LiteralContext AST node
21
66
  * @param _input - Read-only context (unused for literals)
22
- * @param _state - Current generation state (unused for literals)
67
+ * @param state - Current generation state (contains expectedType)
23
68
  * @param _orchestrator - For delegating to other generators (unused for literals)
24
69
  * @returns Generated code and effects (stdbool include for bool literals)
25
70
  */
26
71
  const generateLiteral = (
27
72
  node: LiteralContext,
28
73
  _input: IGeneratorInput,
29
- _state: IGeneratorState,
74
+ state: IGeneratorState,
30
75
  _orchestrator: IOrchestrator,
31
76
  ): IGeneratorOutput => {
32
77
  const effects: TGeneratorEffect[] = [];
@@ -35,30 +80,53 @@ const generateLiteral = (
35
80
  // Track boolean literal usage to include stdbool.h
36
81
  if (literalText === "true" || literalText === "false") {
37
82
  effects.push({ type: "include", header: "stdbool" });
83
+ return { code: literalText, effects };
38
84
  }
39
85
 
40
86
  // ADR-024: Transform C-Next float suffixes to standard C syntax
41
87
  // 3.14f32 -> 3.14f (C float)
42
88
  // 3.14f64 -> 3.14 (C double, no suffix needed)
43
- if (/[fF]32$/.exec(literalText)) {
89
+ if (/[fF]32$/.test(literalText)) {
44
90
  literalText = literalText.replace(/[fF]32$/, "f");
45
- } else if (/[fF]64$/.exec(literalText)) {
91
+ return { code: literalText, effects };
92
+ }
93
+ if (/[fF]64$/.test(literalText)) {
46
94
  literalText = literalText.replace(/[fF]64$/, "");
95
+ return { code: literalText, effects };
47
96
  }
48
97
 
49
98
  // Issue #130: Transform C-Next integer suffixes to standard C syntax
50
99
  // u8/u16/u32 and i8/i16/i32 suffixes are stripped (C infers from context)
51
100
  // u64 -> ULL suffix for 64-bit unsigned
52
101
  // i64 -> LL suffix for 64-bit signed
53
- if (/[uU]64$/.exec(literalText)) {
102
+ if (/[uU]64$/.test(literalText)) {
54
103
  literalText = literalText.replace(/[uU]64$/, "ULL");
55
- } else if (/[iI]64$/.exec(literalText)) {
104
+ return { code: literalText, effects };
105
+ }
106
+ if (/[iI]64$/.test(literalText)) {
56
107
  literalText = literalText.replace(/[iI]64$/, "LL");
57
- } else if (/[uUiI](8|16|32)$/.exec(literalText)) {
58
- // Strip 8/16/32-bit suffixes - C handles these without explicit suffix
108
+ return { code: literalText, effects };
109
+ }
110
+ if (/[uUiI](8|16|32)$/.test(literalText)) {
111
+ // Strip 8/16/32-bit suffixes - will add U below if needed for MISRA
59
112
  literalText = literalText.replace(/[uUiI](8|16|32)$/, "");
60
113
  }
61
114
 
115
+ // MISRA Rule 7.2: Add unsigned suffix based on expectedType
116
+ // Only applies to numeric integer literals without existing unsigned suffix
117
+ const expectedType = state?.expectedType;
118
+ if (
119
+ expectedType &&
120
+ isNumericIntegerLiteral(literalText) &&
121
+ !hasUnsignedSuffix(literalText)
122
+ ) {
123
+ if (UNSIGNED_64_TYPES.has(expectedType)) {
124
+ literalText = literalText + "ULL";
125
+ } else if (UNSIGNED_TYPES.has(expectedType)) {
126
+ literalText = literalText + "U";
127
+ }
128
+ }
129
+
62
130
  return { code: literalText, effects };
63
131
  };
64
132
 
@@ -1270,4 +1270,151 @@ describe("CallExprGenerator", () => {
1270
1270
  expect(markParameterModified).not.toHaveBeenCalled();
1271
1271
  });
1272
1272
  });
1273
+
1274
+ // ========================================================================
1275
+ // Issue #832: Auto-reference for typedef pointer output parameters
1276
+ // ========================================================================
1277
+ describe("Issue #832: typedef pointer output parameters", () => {
1278
+ it("adds & when typedef pointer type is passed to pointer-to-typedef param", () => {
1279
+ // handle_t is typedef'd pointer, create_handle expects handle_t*
1280
+ const argExprs = [createMockExpressionContext("my_handle")];
1281
+ const argCtx = createMockArgListContext(argExprs);
1282
+ const sigs = new Map([
1283
+ [
1284
+ "create_handle",
1285
+ {
1286
+ name: "create_handle",
1287
+ parameters: [
1288
+ {
1289
+ name: "out",
1290
+ baseType: "handle_t*",
1291
+ isConst: false,
1292
+ isArray: false,
1293
+ },
1294
+ ],
1295
+ },
1296
+ ],
1297
+ ]);
1298
+ const typeRegistry = new Map([
1299
+ [
1300
+ "my_handle",
1301
+ { baseType: "handle_t", bitWidth: 0, isArray: false, isConst: false },
1302
+ ],
1303
+ ]);
1304
+ const input = createMockInput({ functionSignatures: sigs, typeRegistry });
1305
+ const state = createMockState();
1306
+ const orchestrator = createMockOrchestrator({
1307
+ isCNextFunction: vi.fn(() => false),
1308
+ getExpressionType: vi.fn(() => "handle_t"),
1309
+ isStructType: vi.fn(() => false), // typedef pointer is not a struct
1310
+ isIntegerType: vi.fn(() => false),
1311
+ isFloatType: vi.fn(() => false),
1312
+ });
1313
+
1314
+ const result = generateFunctionCall(
1315
+ "create_handle",
1316
+ argCtx,
1317
+ input,
1318
+ state,
1319
+ orchestrator,
1320
+ );
1321
+
1322
+ expect(result.code).toBe("create_handle(&my_handle)");
1323
+ });
1324
+
1325
+ it("does not add & for primitive types passed to pointer params (array decay)", () => {
1326
+ // uint8_t[] passed to uint8_t* should NOT get &
1327
+ const argExprs = [createMockExpressionContext("data")];
1328
+ const argCtx = createMockArgListContext(argExprs);
1329
+ const sigs = new Map([
1330
+ [
1331
+ "send_data",
1332
+ {
1333
+ name: "send_data",
1334
+ parameters: [
1335
+ {
1336
+ name: "buf",
1337
+ baseType: "uint8_t*",
1338
+ isConst: false,
1339
+ isArray: false,
1340
+ },
1341
+ ],
1342
+ },
1343
+ ],
1344
+ ]);
1345
+ const typeRegistry = new Map([
1346
+ [
1347
+ "data",
1348
+ { baseType: "uint8_t", bitWidth: 8, isArray: true, isConst: false },
1349
+ ],
1350
+ ]);
1351
+ const input = createMockInput({ functionSignatures: sigs, typeRegistry });
1352
+ const state = createMockState();
1353
+ const orchestrator = createMockOrchestrator({
1354
+ isCNextFunction: vi.fn(() => false),
1355
+ getExpressionType: vi.fn(() => "uint8_t"),
1356
+ isStructType: vi.fn(() => false),
1357
+ isIntegerType: vi.fn(() => false), // uint8_t is C type, not in INTEGER_TYPES
1358
+ isFloatType: vi.fn(() => false),
1359
+ });
1360
+
1361
+ const result = generateFunctionCall(
1362
+ "send_data",
1363
+ argCtx,
1364
+ input,
1365
+ state,
1366
+ orchestrator,
1367
+ );
1368
+
1369
+ // Should NOT add & because uint8_t is in C_TYPE_WIDTH (primitive)
1370
+ expect(result.code).toBe("send_data(data)");
1371
+ });
1372
+
1373
+ it("does not add & when typedef type is passed directly (not to pointer)", () => {
1374
+ // use_handle expects handle_t, not handle_t*
1375
+ const argExprs = [createMockExpressionContext("my_handle")];
1376
+ const argCtx = createMockArgListContext(argExprs);
1377
+ const sigs = new Map([
1378
+ [
1379
+ "use_handle",
1380
+ {
1381
+ name: "use_handle",
1382
+ parameters: [
1383
+ {
1384
+ name: "h",
1385
+ baseType: "handle_t", // NOT a pointer
1386
+ isConst: false,
1387
+ isArray: false,
1388
+ },
1389
+ ],
1390
+ },
1391
+ ],
1392
+ ]);
1393
+ const typeRegistry = new Map([
1394
+ [
1395
+ "my_handle",
1396
+ { baseType: "handle_t", bitWidth: 0, isArray: false, isConst: false },
1397
+ ],
1398
+ ]);
1399
+ const input = createMockInput({ functionSignatures: sigs, typeRegistry });
1400
+ const state = createMockState();
1401
+ const orchestrator = createMockOrchestrator({
1402
+ isCNextFunction: vi.fn(() => false),
1403
+ getExpressionType: vi.fn(() => "handle_t"),
1404
+ isStructType: vi.fn(() => false),
1405
+ isIntegerType: vi.fn(() => false),
1406
+ isFloatType: vi.fn(() => false),
1407
+ });
1408
+
1409
+ const result = generateFunctionCall(
1410
+ "use_handle",
1411
+ argCtx,
1412
+ input,
1413
+ state,
1414
+ orchestrator,
1415
+ );
1416
+
1417
+ expect(result.code).toBe("use_handle(my_handle)");
1418
+ });
1419
+ });
1273
1420
  });
@@ -165,4 +165,120 @@ describe("LiteralGenerator", () => {
165
165
  expect(result.effects).toEqual([]);
166
166
  });
167
167
  });
168
+
169
+ describe("MISRA Rule 7.2: unsigned suffix for unsigned types", () => {
170
+ /**
171
+ * Create mock state with expectedType set.
172
+ */
173
+ function createStateWithExpectedType(
174
+ expectedType: string | null,
175
+ ): IGeneratorState {
176
+ return { expectedType } as IGeneratorState;
177
+ }
178
+
179
+ it("should add U suffix to decimal literal when expectedType is u8", () => {
180
+ const node = createMockLiteral("255");
181
+ const state = createStateWithExpectedType("u8");
182
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
183
+
184
+ expect(result.code).toBe("255U");
185
+ });
186
+
187
+ it("should add U suffix to decimal literal when expectedType is u16", () => {
188
+ const node = createMockLiteral("60000");
189
+ const state = createStateWithExpectedType("u16");
190
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
191
+
192
+ expect(result.code).toBe("60000U");
193
+ });
194
+
195
+ it("should add U suffix to decimal literal when expectedType is u32", () => {
196
+ const node = createMockLiteral("4000000000");
197
+ const state = createStateWithExpectedType("u32");
198
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
199
+
200
+ expect(result.code).toBe("4000000000U");
201
+ });
202
+
203
+ it("should add ULL suffix to decimal literal when expectedType is u64", () => {
204
+ const node = createMockLiteral("42");
205
+ const state = createStateWithExpectedType("u64");
206
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
207
+
208
+ expect(result.code).toBe("42ULL");
209
+ });
210
+
211
+ it("should add U suffix to hex literal when expectedType is u8", () => {
212
+ const node = createMockLiteral("0xFF");
213
+ const state = createStateWithExpectedType("u8");
214
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
215
+
216
+ expect(result.code).toBe("0xFFU");
217
+ });
218
+
219
+ it("should add U suffix to binary literal when expectedType is u8", () => {
220
+ const node = createMockLiteral("0b11110000");
221
+ const state = createStateWithExpectedType("u8");
222
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
223
+
224
+ expect(result.code).toBe("0b11110000U");
225
+ });
226
+
227
+ it("should NOT add U suffix when expectedType is signed (i32)", () => {
228
+ const node = createMockLiteral("42");
229
+ const state = createStateWithExpectedType("i32");
230
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
231
+
232
+ expect(result.code).toBe("42");
233
+ });
234
+
235
+ it("should NOT add U suffix when expectedType is null", () => {
236
+ const node = createMockLiteral("42");
237
+ const state = createStateWithExpectedType(null);
238
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
239
+
240
+ expect(result.code).toBe("42");
241
+ });
242
+
243
+ it("should NOT add U suffix to string literals even with unsigned expectedType", () => {
244
+ const node = createMockLiteral('"hello"');
245
+ const state = createStateWithExpectedType("u8");
246
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
247
+
248
+ expect(result.code).toBe('"hello"');
249
+ });
250
+
251
+ it("should NOT add U suffix to float literals even with unsigned expectedType", () => {
252
+ const node = createMockLiteral("3.14");
253
+ const state = createStateWithExpectedType("u32");
254
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
255
+
256
+ expect(result.code).toBe("3.14");
257
+ });
258
+
259
+ it("should NOT double-add U suffix if already present via explicit suffix", () => {
260
+ const node = createMockLiteral("42u32");
261
+ const state = createStateWithExpectedType("u32");
262
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
263
+
264
+ // u32 suffix is stripped, then U is added based on expectedType
265
+ expect(result.code).toBe("42U");
266
+ });
267
+
268
+ it("should handle uint8_t C type as unsigned", () => {
269
+ const node = createMockLiteral("42");
270
+ const state = createStateWithExpectedType("uint8_t");
271
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
272
+
273
+ expect(result.code).toBe("42U");
274
+ });
275
+
276
+ it("should handle uint32_t C type as unsigned", () => {
277
+ const node = createMockLiteral("0x80000000");
278
+ const state = createStateWithExpectedType("uint32_t");
279
+ const result = generateLiteral(node, mockInput, state, mockOrchestrator);
280
+
281
+ expect(result.code).toBe("0x80000000U");
282
+ });
283
+ });
168
284
  });
@@ -116,17 +116,18 @@ class AssignmentExpectedTypeResolver {
116
116
  let currentStructType: string | undefined = rootTypeInfo.baseType;
117
117
 
118
118
  // Walk through each member in the chain to find the final field's type
119
+ // Issue #831: Use SymbolTable as single source of truth for struct fields
119
120
  for (let i = 1; i < identifiers.length && currentStructType; i++) {
120
121
  const memberName = identifiers[i];
121
- const structFieldTypes: ReadonlyMap<string, string> | undefined =
122
- CodeGenState.symbols?.structFields.get(currentStructType);
122
+ const memberType = CodeGenState.symbolTable?.getStructFieldType(
123
+ currentStructType,
124
+ memberName,
125
+ );
123
126
 
124
- if (!structFieldTypes?.has(memberName)) {
127
+ if (!memberType) {
125
128
  break;
126
129
  }
127
130
 
128
- const memberType: string = structFieldTypes.get(memberName)!;
129
-
130
131
  if (i === identifiers.length - 1) {
131
132
  // Last field in chain - this is the assignment target's type
132
133
  return { expectedType: memberType, assignmentContext: null };
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach } from "vitest";
2
2
  import AssignmentExpectedTypeResolver from "../AssignmentExpectedTypeResolver.js";
3
3
  import CNextSourceParser from "../../../../logic/parser/CNextSourceParser.js";
4
4
  import CodeGenState from "../../../../state/CodeGenState.js";
5
+ import SymbolTable from "../../../../logic/symbols/SymbolTable.js";
5
6
 
6
7
  /**
7
8
  * Create a mock assignment target context by parsing a minimal assignment statement.
@@ -18,12 +19,24 @@ function parseAssignmentTarget(target: string) {
18
19
  }
19
20
 
20
21
  /**
21
- * Helper to set up struct fields in CodeGenState.symbols
22
+ * Helper to set up struct fields in CodeGenState.symbolTable
23
+ * Issue #831: SymbolTable is now the single source of truth for struct fields
22
24
  */
23
25
  function setupStructFields(
24
26
  structName: string,
25
27
  fields: Map<string, string>,
26
28
  ): void {
29
+ // Initialize symbolTable if not set
30
+ if (!CodeGenState.symbolTable) {
31
+ CodeGenState.symbolTable = new SymbolTable();
32
+ }
33
+
34
+ // Register struct fields in SymbolTable
35
+ for (const [fieldName, fieldType] of fields) {
36
+ CodeGenState.symbolTable.addStructField(structName, fieldName, fieldType);
37
+ }
38
+
39
+ // Also mark struct as known (for isKnownStruct checks)
27
40
  if (!CodeGenState.symbols) {
28
41
  CodeGenState.symbols = {
29
42
  knownStructs: new Set(),
@@ -54,10 +67,6 @@ function setupStructFields(
54
67
  };
55
68
  }
56
69
  (CodeGenState.symbols.knownStructs as Set<string>).add(structName);
57
- (CodeGenState.symbols.structFields as Map<string, Map<string, string>>).set(
58
- structName,
59
- fields,
60
- );
61
70
  }
62
71
 
63
72
  describe("AssignmentExpectedTypeResolver", () => {
@@ -58,6 +58,14 @@ interface IFileSystem {
58
58
  * @throws Error if file doesn't exist or can't be read
59
59
  */
60
60
  stat(path: string): { mtimeMs: number };
61
+
62
+ /**
63
+ * Resolve symlinks to get the real path.
64
+ * Optional - if not provided, symlink loop detection is skipped.
65
+ * @returns The resolved real path
66
+ * @throws Error if path doesn't exist
67
+ */
68
+ realpath?(path: string): string;
61
69
  }
62
70
 
63
71
  export default IFileSystem;