c-next 0.2.8 → 0.2.9

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 (24) hide show
  1. package/dist/index.js +213 -69
  2. package/dist/index.js.map +2 -2
  3. package/package.json +1 -1
  4. package/src/transpiler/Transpiler.ts +4 -1
  5. package/src/transpiler/logic/IncludeExtractor.ts +12 -6
  6. package/src/transpiler/logic/__tests__/IncludeExtractor.test.ts +24 -0
  7. package/src/transpiler/output/codegen/CodeGenerator.ts +32 -24
  8. package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +3 -3
  9. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +16 -16
  10. package/src/transpiler/output/codegen/generators/declarationGenerators/ScopeGenerator.ts +8 -1
  11. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +21 -6
  12. package/src/transpiler/output/codegen/generators/expressions/LiteralGenerator.ts +15 -2
  13. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +34 -11
  14. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +2 -1
  15. package/src/transpiler/output/codegen/generators/support/IncludeGenerator.ts +19 -7
  16. package/src/transpiler/output/codegen/generators/support/__tests__/IncludeGenerator.test.ts +68 -0
  17. package/src/transpiler/output/codegen/helpers/ArgumentGenerator.ts +14 -2
  18. package/src/transpiler/output/codegen/helpers/ArrayInitHelper.ts +3 -5
  19. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +56 -8
  20. package/src/transpiler/output/codegen/helpers/VariableDeclHelper.ts +5 -6
  21. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +60 -2
  22. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +14 -16
  23. package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +25 -0
  24. package/src/transpiler/state/CodeGenState.ts +40 -0
@@ -156,6 +156,44 @@ describe("IncludeGenerator", () => {
156
156
 
157
157
  expect(result).toBe("#include <lib.h>");
158
158
  });
159
+
160
+ it("transforms angle bracket .cnx include to .hpp in C++ mode", () => {
161
+ const result = transformIncludeDirective("#include <utils.cnx>", {
162
+ sourcePath: null,
163
+ cppMode: true,
164
+ });
165
+ expect(result).toBe("#include <utils.hpp>");
166
+ });
167
+
168
+ it("resolves path from inputs with .hpp in C++ mode", () => {
169
+ vi.mocked(CnxFileResolver.findCnxFile).mockReturnValue(
170
+ "/project/src/Display/utils.cnx",
171
+ );
172
+ vi.mocked(CnxFileResolver.getRelativePathFromInputs).mockReturnValue(
173
+ "Display/utils.cnx",
174
+ );
175
+
176
+ const result = transformIncludeDirective("#include <utils.cnx>", {
177
+ sourcePath: "/project/src/main.cnx",
178
+ includeDirs: ["/project/src/Display"],
179
+ inputs: ["/project/src"],
180
+ cppMode: true,
181
+ });
182
+
183
+ expect(result).toBe("#include <Display/utils.hpp>");
184
+ });
185
+
186
+ it("falls back to .hpp in C++ mode when file not found", () => {
187
+ vi.mocked(CnxFileResolver.findCnxFile).mockReturnValue(null);
188
+
189
+ const result = transformIncludeDirective("#include <missing.cnx>", {
190
+ sourcePath: "/project/src/main.cnx",
191
+ inputs: ["/project/src"],
192
+ cppMode: true,
193
+ });
194
+
195
+ expect(result).toBe("#include <missing.hpp>");
196
+ });
159
197
  });
160
198
 
161
199
  // ==========================================================================
@@ -221,6 +259,28 @@ describe("IncludeGenerator", () => {
221
259
  }),
222
260
  ).toThrow(/Referenced in:.*main\.cnx/);
223
261
  });
262
+
263
+ it("transforms quoted .cnx include to .hpp in C++ mode", () => {
264
+ vi.mocked(CnxFileResolver.cnxFileExists).mockReturnValue(true);
265
+
266
+ const result = transformIncludeDirective('#include "helper.cnx"', {
267
+ sourcePath: "/project/src/main.cnx",
268
+ cppMode: true,
269
+ });
270
+
271
+ expect(result).toBe('#include "helper.hpp"');
272
+ });
273
+
274
+ it("transforms quoted include with relative path to .hpp in C++ mode", () => {
275
+ vi.mocked(CnxFileResolver.cnxFileExists).mockReturnValue(true);
276
+
277
+ const result = transformIncludeDirective('#include "../lib/utils.cnx"', {
278
+ sourcePath: "/project/src/main.cnx",
279
+ cppMode: true,
280
+ });
281
+
282
+ expect(result).toBe('#include "../lib/utils.hpp"');
283
+ });
224
284
  });
225
285
 
226
286
  // ==========================================================================
@@ -262,6 +322,14 @@ describe("IncludeGenerator", () => {
262
322
  });
263
323
  expect(result).toBe("int x = 5;");
264
324
  });
325
+
326
+ it("passes through .h includes unchanged even in C++ mode", () => {
327
+ const result = transformIncludeDirective('#include "myheader.h"', {
328
+ sourcePath: "/project/main.cnx",
329
+ cppMode: true,
330
+ });
331
+ expect(result).toBe('#include "myheader.h"');
332
+ });
265
333
  });
266
334
 
267
335
  // ==========================================================================
@@ -66,22 +66,34 @@ class ArgumentGenerator {
66
66
 
67
67
  /**
68
68
  * Handle rvalue argument (literals or complex expressions).
69
+ * Issue #872: Sets expectedType for MISRA 7.2 U suffix on unsigned literals.
69
70
  */
70
71
  static handleRvalueArg(
71
72
  ctx: Parser.ExpressionContext,
72
73
  targetParamBaseType: string | undefined,
73
74
  callbacks: IArgumentGeneratorCallbacks,
74
75
  ): string {
76
+ // Issue #872: Early return when no target type - no state management needed
75
77
  if (!targetParamBaseType) {
76
78
  return callbacks.generateExpression(ctx);
77
79
  }
78
80
 
79
81
  const cType = TYPE_MAP[targetParamBaseType];
80
82
  if (!cType || cType === "void") {
81
- return callbacks.generateExpression(ctx);
83
+ // Issue #872: Suppress bare enum resolution in function args (requires ADR to change)
84
+ return CodeGenState.withExpectedType(
85
+ targetParamBaseType,
86
+ () => callbacks.generateExpression(ctx),
87
+ true, // suppressEnumResolution
88
+ );
82
89
  }
83
90
 
84
- const value = callbacks.generateExpression(ctx);
91
+ // Issue #872: Suppress bare enum resolution in function args (requires ADR to change)
92
+ const value = CodeGenState.withExpectedType(
93
+ targetParamBaseType,
94
+ () => callbacks.generateExpression(ctx),
95
+ true, // suppressEnumResolution
96
+ );
85
97
 
86
98
  // C++ mode: rvalues can bind to const T&
87
99
  if (CodeGenState.cppMode) {
@@ -106,11 +106,9 @@ class ArrayInitHelper {
106
106
  callbacks: IArrayInitCallbacks,
107
107
  ): string {
108
108
  const typeName = callbacks.getTypeName(typeCtx);
109
- const savedExpectedType = CodeGenState.expectedType;
110
- CodeGenState.expectedType = typeName;
111
- const initValue = callbacks.generateExpression(expression);
112
- CodeGenState.expectedType = savedExpectedType;
113
- return initValue;
109
+ return CodeGenState.withExpectedType(typeName, () =>
110
+ callbacks.generateExpression(expression),
111
+ );
114
112
  }
115
113
 
116
114
  /**
@@ -54,22 +54,36 @@ class AssignmentExpectedTypeResolver {
54
54
  return AssignmentExpectedTypeResolver.resolveForSimpleIdentifier(baseId);
55
55
  }
56
56
 
57
- // Case 2: Has member access - extract identifiers from postfix chain
57
+ // Case 2: Has postfix ops - extract identifiers from chain
58
58
  if (baseId && postfixOps.length > 0) {
59
59
  const { identifiers, hasSubscript } = analyzePostfixOps(
60
60
  baseId,
61
61
  postfixOps,
62
62
  );
63
63
 
64
- // If we have member access (multiple identifiers), resolve for member chain
64
+ // Case 2a: Member access only (no subscript)
65
65
  if (identifiers.length >= 2 && !hasSubscript) {
66
66
  return AssignmentExpectedTypeResolver.resolveForMemberChain(
67
67
  identifiers,
68
68
  );
69
69
  }
70
+
71
+ // Case 2b: Simple array element access (arr[i] <- value)
72
+ // Issue #872: Resolve element type for MISRA 7.2 U suffix
73
+ if (identifiers.length === 1 && hasSubscript) {
74
+ return AssignmentExpectedTypeResolver.resolveForArrayElement(baseId);
75
+ }
76
+
77
+ // Case 2c: Member chain with array access (struct.arr[i] <- value)
78
+ // Issue #872: Walk chain and resolve element type
79
+ if (identifiers.length >= 2 && hasSubscript) {
80
+ return AssignmentExpectedTypeResolver.resolveForMemberArrayElement(
81
+ identifiers,
82
+ );
83
+ }
70
84
  }
71
85
 
72
- // Case 3: Array access or complex patterns - no expected type resolution
86
+ // Case 3: Complex patterns we can't resolve
73
87
  return { expectedType: null, assignmentContext: null };
74
88
  }
75
89
 
@@ -98,10 +112,49 @@ class AssignmentExpectedTypeResolver {
98
112
  *
99
113
  * Issue #452: Enables type-aware resolution of unqualified enum members
100
114
  * for nested access (e.g., config.nested.field).
115
+ *
116
+ * Delegates to walkMemberChain shared implementation.
101
117
  */
102
118
  private static resolveForMemberChain(
103
119
  identifiers: string[],
104
120
  ): IExpectedTypeResult {
121
+ return AssignmentExpectedTypeResolver.walkMemberChain(identifiers);
122
+ }
123
+
124
+ /**
125
+ * Resolve expected type for array element access.
126
+ * Issue #872: arr[i] <- value needs baseType for MISRA 7.2 U suffix.
127
+ */
128
+ private static resolveForArrayElement(id: string): IExpectedTypeResult {
129
+ const typeInfo = CodeGenState.getVariableTypeInfo(id);
130
+ if (!typeInfo?.isArray) {
131
+ return { expectedType: null, assignmentContext: null };
132
+ }
133
+
134
+ // Element type is the baseType (e.g., u8[10] -> "u8")
135
+ return { expectedType: typeInfo.baseType, assignmentContext: null };
136
+ }
137
+
138
+ /**
139
+ * Resolve expected type for member chain ending with array access.
140
+ * Issue #872: struct.arr[i] <- value needs element type for MISRA 7.2.
141
+ *
142
+ * Delegates to walkMemberChain which handles both member chain and
143
+ * member-array-element patterns identically (both return final field type).
144
+ */
145
+ private static resolveForMemberArrayElement(
146
+ identifiers: string[],
147
+ ): IExpectedTypeResult {
148
+ return AssignmentExpectedTypeResolver.walkMemberChain(identifiers);
149
+ }
150
+
151
+ /**
152
+ * Walk a struct member chain to find the final field's type.
153
+ * Shared implementation for both member chain and member-array-element patterns.
154
+ *
155
+ * Issue #831: Uses SymbolTable as single source of truth for struct fields.
156
+ */
157
+ private static walkMemberChain(identifiers: string[]): IExpectedTypeResult {
105
158
  if (identifiers.length < 2) {
106
159
  return { expectedType: null, assignmentContext: null };
107
160
  }
@@ -115,8 +168,6 @@ class AssignmentExpectedTypeResolver {
115
168
 
116
169
  let currentStructType: string | undefined = rootTypeInfo.baseType;
117
170
 
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
120
171
  for (let i = 1; i < identifiers.length && currentStructType; i++) {
121
172
  const memberName = identifiers[i];
122
173
  const memberType = CodeGenState.symbolTable?.getStructFieldType(
@@ -129,13 +180,10 @@ class AssignmentExpectedTypeResolver {
129
180
  }
130
181
 
131
182
  if (i === identifiers.length - 1) {
132
- // Last field in chain - this is the assignment target's type
133
183
  return { expectedType: memberType, assignmentContext: null };
134
184
  } else if (CodeGenState.isKnownStruct(memberType)) {
135
- // Intermediate field - continue walking if it's a struct
136
185
  currentStructType = memberType;
137
186
  } else {
138
- // Intermediate field is not a struct - can't walk further
139
187
  break;
140
188
  }
141
189
  }
@@ -522,8 +522,6 @@ class VariableDeclHelper {
522
522
  }
523
523
 
524
524
  const typeName = callbacks.getTypeName(typeCtx);
525
- const savedExpectedType = CodeGenState.expectedType;
526
- CodeGenState.expectedType = typeName;
527
525
 
528
526
  // ADR-017: Validate enum type for initialization
529
527
  EnumAssignmentValidator.validateEnumAssignment(typeName, ctx.expression()!);
@@ -533,10 +531,11 @@ class VariableDeclHelper {
533
531
  getExpressionType: callbacks.getExpressionType,
534
532
  });
535
533
 
536
- const result = `${decl} = ${callbacks.generateExpression(ctx.expression()!)}`;
537
- CodeGenState.expectedType = savedExpectedType;
538
-
539
- return result;
534
+ // Issue #872: Set expectedType for MISRA 7.2 U suffix compliance
535
+ return CodeGenState.withExpectedType(
536
+ typeName,
537
+ () => `${decl} = ${callbacks.generateExpression(ctx.expression()!)}`,
538
+ );
540
539
  }
541
540
 
542
541
  // ========================================================================
@@ -183,7 +183,8 @@ describe("AssignmentExpectedTypeResolver", () => {
183
183
  });
184
184
 
185
185
  describe("array access", () => {
186
- it("should return null for array access target", () => {
186
+ // Issue #872: Array element assignments need expectedType for MISRA 7.2 U suffix
187
+ it("should resolve expected type for simple array element access", () => {
187
188
  CodeGenState.setVariableTypeInfo("arr", {
188
189
  baseType: "u32",
189
190
  bitWidth: 32,
@@ -194,8 +195,65 @@ describe("AssignmentExpectedTypeResolver", () => {
194
195
 
195
196
  const result = AssignmentExpectedTypeResolver.resolve(target);
196
197
 
198
+ expect(result.expectedType).toBe("u32");
199
+ });
200
+
201
+ it("should resolve expected type for u8 array element access", () => {
202
+ CodeGenState.setVariableTypeInfo("buffer", {
203
+ baseType: "u8",
204
+ bitWidth: 8,
205
+ isArray: true,
206
+ isConst: false,
207
+ });
208
+ const target = parseAssignmentTarget("buffer[5]");
209
+
210
+ const result = AssignmentExpectedTypeResolver.resolve(target);
211
+
212
+ expect(result.expectedType).toBe("u8");
213
+ });
214
+
215
+ it("should resolve expected type for struct member array access", () => {
216
+ CodeGenState.setVariableTypeInfo("pkt", {
217
+ baseType: "Packet",
218
+ bitWidth: 0,
219
+ isArray: false,
220
+ isConst: false,
221
+ });
222
+ setupStructFields("Packet", new Map([["header", "u8"]]));
223
+ // Mark header as an array field
224
+ if (CodeGenState.symbols) {
225
+ (
226
+ CodeGenState.symbols.structFieldArrays as Map<string, Set<string>>
227
+ ).set("Packet", new Set(["header"]));
228
+ }
229
+ const target = parseAssignmentTarget("pkt.header[0]");
230
+
231
+ const result = AssignmentExpectedTypeResolver.resolve(target);
232
+
233
+ expect(result.expectedType).toBe("u8");
234
+ });
235
+
236
+ it("should resolve expected type for multi-dimensional array element", () => {
237
+ CodeGenState.setVariableTypeInfo("matrix", {
238
+ baseType: "u8",
239
+ bitWidth: 8,
240
+ isArray: true,
241
+ arrayDimensions: [4, 8],
242
+ isConst: false,
243
+ });
244
+ const target = parseAssignmentTarget("matrix[0][0]");
245
+
246
+ const result = AssignmentExpectedTypeResolver.resolve(target);
247
+
248
+ expect(result.expectedType).toBe("u8");
249
+ });
250
+
251
+ it("should return null for unknown array variable", () => {
252
+ const target = parseAssignmentTarget("unknown[0]");
253
+
254
+ const result = AssignmentExpectedTypeResolver.resolve(target);
255
+
197
256
  expect(result.expectedType).toBeNull();
198
- expect(result.assignmentContext).toBeNull();
199
257
  });
200
258
  });
201
259
  });
@@ -277,29 +277,27 @@ class HeaderGeneratorUtils {
277
277
  lines.push("#include <stdint.h>", "#include <stdbool.h>");
278
278
  }
279
279
 
280
- // User includes
281
- // Issue #933: Transform .h to .hpp in C++ mode so headers include correct files
280
+ // User includes (already have correct extension from IncludeExtractor)
282
281
  if (options.userIncludes && options.userIncludes.length > 0) {
283
282
  for (const include of options.userIncludes) {
284
- const transformedInclude = options.cppMode
285
- ? include.replace(/\.h"/, '.hpp"').replace(/\.h>/, ".hpp>")
286
- : include;
287
- lines.push(transformedInclude);
283
+ lines.push(include);
288
284
  }
289
285
  }
290
286
 
291
287
  // External type header includes (skip duplicates of user includes)
292
- // Note: External C headers stay as .h - only user includes from .cnx files get
293
- // transformed to .hpp in C++ mode (those are C-Next generated headers)
294
- const userIncludeSet = new Set(
295
- options.userIncludes?.map((inc) =>
296
- options.cppMode
297
- ? inc.replace(/\.h"/, '.hpp"').replace(/\.h>/, ".hpp>")
298
- : inc,
299
- ) ?? [],
300
- );
288
+ // Note: headersToInclude comes from IncludeResolver which always produces .h
289
+ // (it runs before cppDetected is set). We need to match against userIncludes
290
+ // which may have .hpp in C++ mode, so normalize both for deduplication.
291
+ const userIncludeSet = new Set(options.userIncludes ?? []);
292
+ // Issue #941: Also add .h variants for dedup since headersToInclude uses .h
293
+ if (options.cppMode && options.userIncludes) {
294
+ for (const inc of options.userIncludes) {
295
+ // Add the .h version so we can match against headersToInclude
296
+ const hVersion = inc.replace(/\.hpp"/, '.h"').replace(/\.hpp>/, ".h>");
297
+ userIncludeSet.add(hVersion);
298
+ }
299
+ }
301
300
  for (const directive of headersToInclude) {
302
- // Don't transform external C headers - they should stay as .h
303
301
  if (!userIncludeSet.has(directive)) {
304
302
  lines.push(directive);
305
303
  }
@@ -542,6 +542,31 @@ describe("HeaderGeneratorUtils", () => {
542
542
 
543
543
  expect(lines[lines.length - 1]).toBe("");
544
544
  });
545
+
546
+ it("passes through userIncludes as-is (extension already correct from IncludeExtractor)", () => {
547
+ const lines = HeaderGeneratorUtils.generateIncludes(
548
+ {
549
+ userIncludes: ['#include "types.hpp"'],
550
+ cppMode: true,
551
+ },
552
+ new Set(),
553
+ );
554
+
555
+ expect(lines).toContain('#include "types.hpp"');
556
+ });
557
+
558
+ it("deduplicates headersToInclude against userIncludes with .h/.hpp normalization in C++ mode", () => {
559
+ const result = HeaderGeneratorUtils.generateIncludes(
560
+ {
561
+ userIncludes: ['#include "types.hpp"'],
562
+ cppMode: true,
563
+ },
564
+ new Set(['#include "types.h"']),
565
+ );
566
+ // Should NOT have duplicate - the .h version should be filtered
567
+ const typeIncludes = result.filter((l) => l.includes("types"));
568
+ expect(typeIncludes).toEqual(['#include "types.hpp"']);
569
+ });
545
570
  });
546
571
 
547
572
  describe("generateCppWrapperStart", () => {
@@ -220,6 +220,14 @@ export default class CodeGenState {
220
220
  /** Expected type for struct initializers and enum inference */
221
221
  static expectedType: string | null = null;
222
222
 
223
+ /**
224
+ * Suppress bare enum resolution even when expectedType is set.
225
+ * Issue #872: MISRA 7.2 requires expectedType for U suffix on function args,
226
+ * but bare enum resolution in function args was never allowed and changing
227
+ * that would require ADR approval.
228
+ */
229
+ static suppressBareEnumResolution: boolean = false;
230
+
223
231
  /** Track args parameter name for main() translation */
224
232
  static mainArgsName: string | null = null;
225
233
 
@@ -362,6 +370,7 @@ export default class CodeGenState {
362
370
  this.indentLevel = 0;
363
371
  this.inFunctionBody = false;
364
372
  this.expectedType = null;
373
+ this.suppressBareEnumResolution = false;
365
374
  this.mainArgsName = null;
366
375
  this.assignmentContext = {
367
376
  targetName: null,
@@ -421,6 +430,37 @@ export default class CodeGenState {
421
430
  this.floatShadowCurrent.clear();
422
431
  }
423
432
 
433
+ /**
434
+ * Execute a function with a temporary expectedType, restoring on completion.
435
+ * Issue #872: Extracted to eliminate duplicate save/restore pattern and add exception safety.
436
+ *
437
+ * @param type - The expected type to set (if falsy, no change is made)
438
+ * @param fn - The function to execute
439
+ * @param suppressEnumResolution - If true, suppress bare enum resolution (for MISRA-only contexts)
440
+ * @returns The result of the function
441
+ */
442
+ static withExpectedType<T>(
443
+ type: string | undefined | null,
444
+ fn: () => T,
445
+ suppressEnumResolution: boolean = false,
446
+ ): T {
447
+ if (!type) {
448
+ return fn();
449
+ }
450
+ const savedType = this.expectedType;
451
+ const savedSuppress = this.suppressBareEnumResolution;
452
+ this.expectedType = type;
453
+ if (suppressEnumResolution) {
454
+ this.suppressBareEnumResolution = true;
455
+ }
456
+ try {
457
+ return fn();
458
+ } finally {
459
+ this.expectedType = savedType;
460
+ this.suppressBareEnumResolution = savedSuppress;
461
+ }
462
+ }
463
+
424
464
  // ===========================================================================
425
465
  // CONVENIENCE LOOKUP METHODS
426
466
  // ===========================================================================