c-next 0.2.3 → 0.2.5

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 (61) hide show
  1. package/README.md +59 -57
  2. package/dist/index.js +859 -198
  3. package/dist/index.js.map +4 -4
  4. package/package.json +3 -1
  5. package/src/cli/Runner.ts +1 -1
  6. package/src/cli/__tests__/Runner.test.ts +8 -8
  7. package/src/cli/serve/ServeCommand.ts +29 -9
  8. package/src/transpiler/Transpiler.ts +105 -200
  9. package/src/transpiler/__tests__/DualCodePaths.test.ts +117 -68
  10. package/src/transpiler/__tests__/Transpiler.coverage.test.ts +87 -51
  11. package/src/transpiler/__tests__/Transpiler.test.ts +150 -48
  12. package/src/transpiler/__tests__/determineProjectRoot.test.ts +2 -2
  13. package/src/transpiler/data/IncludeResolver.ts +11 -3
  14. package/src/transpiler/data/__tests__/IncludeResolver.test.ts +2 -2
  15. package/src/transpiler/logic/analysis/ArrayIndexTypeAnalyzer.ts +346 -0
  16. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +281 -14
  17. package/src/transpiler/logic/analysis/__tests__/ArrayIndexTypeAnalyzer.test.ts +545 -0
  18. package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +375 -0
  19. package/src/transpiler/logic/analysis/runAnalyzers.ts +9 -2
  20. package/src/transpiler/logic/analysis/types/IArrayIndexTypeError.ts +15 -0
  21. package/src/transpiler/logic/symbols/TransitiveEnumCollector.ts +1 -1
  22. package/src/transpiler/logic/symbols/c/__tests__/CResolver.integration.test.ts +18 -0
  23. package/src/transpiler/logic/symbols/c/index.ts +50 -1
  24. package/src/transpiler/output/codegen/CodeGenerator.ts +69 -15
  25. package/src/transpiler/output/codegen/TypeResolver.ts +1 -1
  26. package/src/transpiler/output/codegen/TypeValidator.ts +10 -7
  27. package/src/transpiler/output/codegen/__tests__/ExpressionWalker.test.ts +9 -3
  28. package/src/transpiler/output/codegen/__tests__/RequireInclude.test.ts +86 -23
  29. package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +3 -1
  30. package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +43 -29
  31. package/src/transpiler/output/codegen/generators/IOrchestrator.ts +5 -2
  32. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +15 -3
  33. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +1 -1
  34. package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +12 -3
  35. package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +4 -4
  36. package/src/transpiler/output/codegen/helpers/FunctionContextManager.ts +64 -3
  37. package/src/transpiler/output/codegen/helpers/MemberSeparatorResolver.ts +28 -6
  38. package/src/transpiler/output/codegen/helpers/ParameterDereferenceResolver.ts +12 -0
  39. package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +30 -2
  40. package/src/transpiler/output/codegen/helpers/ParameterSignatureBuilder.ts +15 -7
  41. package/src/transpiler/output/codegen/helpers/StringOperationsHelper.ts +1 -1
  42. package/src/transpiler/output/codegen/helpers/TypedefParamParser.ts +220 -0
  43. package/src/transpiler/output/codegen/helpers/__tests__/FunctionContextManager.test.ts +5 -5
  44. package/src/transpiler/output/codegen/helpers/__tests__/MemberSeparatorResolver.test.ts +48 -36
  45. package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +37 -0
  46. package/src/transpiler/output/codegen/helpers/__tests__/ParameterSignatureBuilder.test.ts +63 -0
  47. package/src/transpiler/output/codegen/helpers/__tests__/TypedefParamParser.test.ts +209 -0
  48. package/src/transpiler/output/codegen/resolution/EnumTypeResolver.ts +1 -1
  49. package/src/transpiler/output/codegen/resolution/SizeofResolver.ts +1 -1
  50. package/src/transpiler/output/codegen/types/IParameterInput.ts +13 -0
  51. package/src/transpiler/output/codegen/types/ISeparatorContext.ts +7 -0
  52. package/src/transpiler/output/codegen/types/TParameterInfo.ts +12 -0
  53. package/src/transpiler/output/codegen/utils/CodegenParserUtils.ts +1 -1
  54. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +5 -2
  55. package/src/transpiler/state/CodeGenState.ts +25 -0
  56. package/src/transpiler/types/IPipelineFile.ts +2 -2
  57. package/src/transpiler/types/IPipelineInput.ts +1 -1
  58. package/src/transpiler/types/TTranspileInput.ts +18 -0
  59. package/src/{transpiler/output/codegen/utils → utils}/ExpressionUnwrapper.ts +1 -1
  60. package/src/{transpiler/output/codegen/utils → utils}/__tests__/ExpressionUnwrapper.test.ts +2 -2
  61. package/src/utils/constants/TypeConstants.ts +22 -0
@@ -110,6 +110,8 @@ const initializeTrackingState = (
110
110
  interface IPostfixContext {
111
111
  rootIdentifier: string | undefined;
112
112
  isStructParam: boolean;
113
+ /** Issue #895: Force pointer semantics for callback-compatible params */
114
+ forcePointerSemantics: boolean;
113
115
  input: IGeneratorInput;
114
116
  state: IGeneratorState;
115
117
  orchestrator: IOrchestrator;
@@ -149,6 +151,8 @@ const generatePostfixExpression = (
149
151
  ? state.currentParameters.get(rootIdentifier)
150
152
  : null;
151
153
  const isStructParam = paramInfo?.isStruct ?? false;
154
+ // Issue #895: Callback-compatible params need pointer semantics even in C++ mode
155
+ const forcePointerSemantics = paramInfo?.forcePointerSemantics ?? false;
152
156
 
153
157
  // Issue #579: Check if we have subscript access on a non-array parameter
154
158
  const hasSubscriptOps = ops.some((op) => op.expression().length > 0);
@@ -178,6 +182,7 @@ const generatePostfixExpression = (
178
182
  const postfixCtx: IPostfixContext = {
179
183
  rootIdentifier,
180
184
  isStructParam,
185
+ forcePointerSemantics,
181
186
  input,
182
187
  state,
183
188
  orchestrator,
@@ -303,6 +308,7 @@ const handleMemberOp = (
303
308
  memberName,
304
309
  rootIdentifier: ctx.rootIdentifier,
305
310
  isStructParam: ctx.isStructParam,
311
+ forcePointerSemantics: ctx.forcePointerSemantics,
306
312
  isGlobalAccess: tracking.isGlobalAccess,
307
313
  isCppAccessChain: tracking.isCppAccessChain,
308
314
  currentStructType: tracking.currentStructType,
@@ -1409,6 +1415,8 @@ interface IMemberAccessContext {
1409
1415
  memberName: string;
1410
1416
  rootIdentifier: string | undefined;
1411
1417
  isStructParam: boolean;
1418
+ /** Issue #895: Force pointer semantics for callback-compatible params */
1419
+ forcePointerSemantics: boolean;
1412
1420
  isGlobalAccess: boolean;
1413
1421
  isCppAccessChain: boolean;
1414
1422
  currentStructType: string | undefined;
@@ -1657,9 +1665,13 @@ const tryStructParamAccess = (
1657
1665
  return null;
1658
1666
  }
1659
1667
 
1660
- const structParamSep = memberAccessChain.getStructParamSeparator({
1661
- cppMode: orchestrator.isCppMode(),
1662
- });
1668
+ // Issue #895: Force pointer semantics for callback-compatible params
1669
+ // even in C++ mode (use -> instead of .)
1670
+ const structParamSep = ctx.forcePointerSemantics
1671
+ ? "->"
1672
+ : memberAccessChain.getStructParamSeparator({
1673
+ cppMode: orchestrator.isCppMode(),
1674
+ });
1663
1675
 
1664
1676
  const output = initializeMemberOutput(ctx);
1665
1677
  output.result = `${ctx.result}${structParamSep}${ctx.memberName}`;
@@ -200,7 +200,7 @@ function createMockOrchestrator(overrides?: {
200
200
  indent: vi.fn((text) => text),
201
201
  validateNoEarlyExits: vi.fn(),
202
202
  validateSwitchStatement: vi.fn(),
203
- validateDoWhileCondition: vi.fn(),
203
+ validateConditionIsBoolean: vi.fn(),
204
204
  validateConditionNoFunctionCall: vi.fn(),
205
205
  validateTernaryConditionNoFunctionCall: vi.fn(),
206
206
  generateAssignmentTarget: vi.fn(),
@@ -118,6 +118,9 @@ const generateIf = (
118
118
  // Issue #254: Validate no function calls in condition (E0702)
119
119
  orchestrator.validateConditionNoFunctionCall(node.expression(), "if");
120
120
 
121
+ // Issue #884: Validate condition is a boolean expression (E0701)
122
+ orchestrator.validateConditionIsBoolean(node.expression(), "if");
123
+
121
124
  // Generate with cache enabled
122
125
  const condition = orchestrator.generateExpression(node.expression());
123
126
 
@@ -161,6 +164,9 @@ const generateWhile = (
161
164
  // Issue #254: Validate no function calls in condition (E0702)
162
165
  orchestrator.validateConditionNoFunctionCall(node.expression(), "while");
163
166
 
167
+ // Issue #884: Validate condition is a boolean expression (E0701)
168
+ orchestrator.validateConditionIsBoolean(node.expression(), "while");
169
+
164
170
  const condition = orchestrator.generateExpression(node.expression());
165
171
 
166
172
  // Issue #250: Flush any temp vars from condition BEFORE generating body
@@ -189,12 +195,12 @@ const generateDoWhile = (
189
195
  ): IGeneratorOutput => {
190
196
  const effects: TGeneratorEffect[] = [];
191
197
 
192
- // Validate the condition is a boolean expression (E0701)
193
- orchestrator.validateDoWhileCondition(node.expression());
194
-
195
198
  // Issue #254: Validate no function calls in condition (E0702)
196
199
  orchestrator.validateConditionNoFunctionCall(node.expression(), "do-while");
197
200
 
201
+ // Issue #884: Validate condition is a boolean expression (E0701)
202
+ orchestrator.validateConditionIsBoolean(node.expression(), "do-while");
203
+
198
204
  const body = orchestrator.generateBlock(node.block());
199
205
  const condition = orchestrator.generateExpression(node.expression());
200
206
 
@@ -311,6 +317,9 @@ const generateFor = (
311
317
  if (node.expression()) {
312
318
  // Issue #254: Validate no function calls in condition (E0702)
313
319
  orchestrator.validateConditionNoFunctionCall(node.expression()!, "for");
320
+
321
+ // Issue #884: Validate condition is a boolean expression (E0701)
322
+ orchestrator.validateConditionIsBoolean(node.expression()!, "for");
314
323
  condition = orchestrator.generateExpression(node.expression()!);
315
324
  }
316
325
 
@@ -364,7 +364,7 @@ function createMockOrchestrator(options?: {
364
364
  registerLocalVariable: vi.fn(),
365
365
  flushPendingTempDeclarations: vi.fn(() => options?.tempDeclarations ?? ""),
366
366
  validateConditionNoFunctionCall: vi.fn(),
367
- validateDoWhileCondition: vi.fn(),
367
+ validateConditionIsBoolean: vi.fn(),
368
368
  countStringLengthAccesses: vi.fn(() => new Map()),
369
369
  countBlockLengthAccesses: vi.fn(),
370
370
  setupLengthCache: vi.fn(() => options?.lengthCacheDecls ?? ""),
@@ -690,15 +690,15 @@ describe("ControlFlowGenerator", () => {
690
690
  const ctx = createMockDoWhileStatement({ expr });
691
691
  const input = createMockInput();
692
692
  const state = createMockState();
693
- const validateDoWhileCondition = vi.fn();
693
+ const validateConditionIsBoolean = vi.fn();
694
694
  const orchestrator = {
695
695
  ...createMockOrchestrator(),
696
- validateDoWhileCondition,
696
+ validateConditionIsBoolean,
697
697
  } as unknown as IOrchestrator;
698
698
 
699
699
  generateDoWhile(ctx, input, state, orchestrator);
700
700
 
701
- expect(validateDoWhileCondition).toHaveBeenCalledWith(expr);
701
+ expect(validateConditionIsBoolean).toHaveBeenCalledWith(expr, "do-while");
702
702
  });
703
703
 
704
704
  it("validates no function calls in condition (Issue #254)", () => {
@@ -15,6 +15,8 @@ import CodeGenState from "../../../state/CodeGenState.js";
15
15
  import TYPE_WIDTH from "../types/TYPE_WIDTH.js";
16
16
  import ArrayDimensionParser from "./ArrayDimensionParser.js";
17
17
  import IFunctionContextCallbacks from "../types/IFunctionContextCallbacks.js";
18
+ // Issue #895: Parse typedef signatures to determine pointer vs value params
19
+ import TypedefParamParser from "./TypedefParamParser.js";
18
20
 
19
21
  /**
20
22
  * Result from resolving parameter type information.
@@ -124,8 +126,9 @@ class FunctionContextManager {
124
126
  CodeGenState.currentParameters.clear();
125
127
  if (!params) return;
126
128
 
127
- for (const param of params.parameter()) {
128
- FunctionContextManager.processParameter(param, callbacks);
129
+ const paramList = params.parameter();
130
+ for (let i = 0; i < paramList.length; i++) {
131
+ FunctionContextManager.processParameter(paramList[i], callbacks, i);
129
132
  }
130
133
  }
131
134
 
@@ -135,6 +138,7 @@ class FunctionContextManager {
135
138
  static processParameter(
136
139
  param: Parser.ParameterContext,
137
140
  callbacks: IFunctionContextCallbacks,
141
+ paramIndex: number,
138
142
  ): void {
139
143
  const name = param.IDENTIFIER().getText();
140
144
  // Check both C-Next style (u8[8] param) and legacy style (u8 param[8])
@@ -149,15 +153,38 @@ class FunctionContextManager {
149
153
  callbacks,
150
154
  );
151
155
 
156
+ // Issue #895: For callback-compatible functions, check the typedef signature
157
+ // to determine if the param should be a pointer or value
158
+ const callbackTypedefInfo =
159
+ FunctionContextManager.getCallbackTypedefParamInfo(paramIndex);
160
+ const isCallbackPointerParam =
161
+ callbackTypedefInfo?.shouldBePointer ?? false;
162
+
163
+ // Determine isStruct: for callback-compatible params, both typedef AND type info matter
164
+ // - If typedef says pointer AND it's actually a struct, use -> access (isStruct=true)
165
+ // - If typedef says pointer BUT it's a primitive (like u8), don't treat as struct
166
+ // (primitives use forcePointerSemantics for dereference instead)
167
+ const isStruct = callbackTypedefInfo
168
+ ? isCallbackPointerParam && typeInfo.isStruct
169
+ : typeInfo.isStruct;
170
+
171
+ // Issue #895: Primitive types that become pointers need dereferencing when used as values
172
+ // e.g., "u8 buf" becoming "uint8_t* buf" requires "*buf" when accessing the value
173
+ const isCallbackPointerPrimitive =
174
+ isCallbackPointerParam && !typeInfo.isStruct && !isArray;
175
+
152
176
  // Register in currentParameters
153
177
  const paramInfo = {
154
178
  name,
155
179
  baseType: typeInfo.typeName,
156
180
  isArray,
157
- isStruct: typeInfo.isStruct,
181
+ isStruct,
158
182
  isConst,
159
183
  isCallback: typeInfo.isCallback,
160
184
  isString: typeInfo.isString,
185
+ isCallbackPointerPrimitive,
186
+ // Issue #895: Callback-compatible params need pointer semantics even in C++ mode
187
+ forcePointerSemantics: isCallbackPointerParam,
161
188
  };
162
189
  CodeGenState.currentParameters.set(name, paramInfo);
163
190
 
@@ -366,6 +393,40 @@ class FunctionContextManager {
366
393
  return dimensions;
367
394
  }
368
395
 
396
+ /**
397
+ * Issue #895: Get callback typedef parameter info from the C header.
398
+ * Returns null if not callback-compatible or index is invalid.
399
+ */
400
+ static getCallbackTypedefParamInfo(
401
+ paramIndex: number,
402
+ ): { shouldBePointer: boolean; shouldBeConst: boolean } | null {
403
+ if (CodeGenState.currentFunctionName === null) return null;
404
+
405
+ const typedefName = CodeGenState.callbackCompatibleFunctions.get(
406
+ CodeGenState.currentFunctionName,
407
+ );
408
+ if (!typedefName) return null;
409
+
410
+ const typedefType = CodeGenState.getTypedefType(typedefName);
411
+ if (!typedefType) return null;
412
+
413
+ const shouldBePointer = TypedefParamParser.shouldBePointer(
414
+ typedefType,
415
+ paramIndex,
416
+ );
417
+ const shouldBeConst = TypedefParamParser.shouldBeConst(
418
+ typedefType,
419
+ paramIndex,
420
+ );
421
+
422
+ if (shouldBePointer === null) return null;
423
+
424
+ return {
425
+ shouldBePointer,
426
+ shouldBeConst: shouldBeConst ?? false,
427
+ };
428
+ }
429
+
369
430
  /**
370
431
  * Extract string capacity from a string type context.
371
432
  */
@@ -15,6 +15,19 @@
15
15
  import ISeparatorContext from "../types/ISeparatorContext";
16
16
  import IMemberSeparatorDeps from "../types/IMemberSeparatorDeps";
17
17
 
18
+ /**
19
+ * Input parameters for building a separator context
20
+ */
21
+ interface IBuildContextInput {
22
+ firstId: string;
23
+ hasGlobal: boolean;
24
+ hasThis: boolean;
25
+ currentScope: string | null;
26
+ isStructParam: boolean;
27
+ isCppAccess: boolean;
28
+ forcePointerSemantics?: boolean;
29
+ }
30
+
18
31
  /**
19
32
  * Static utility for resolving member access separators
20
33
  */
@@ -23,14 +36,18 @@ class MemberSeparatorResolver {
23
36
  * Build the separator context for a member access chain
24
37
  */
25
38
  static buildContext(
26
- firstId: string,
27
- hasGlobal: boolean,
28
- hasThis: boolean,
29
- currentScope: string | null,
30
- isStructParam: boolean,
39
+ input: IBuildContextInput,
31
40
  deps: IMemberSeparatorDeps,
32
- isCppAccess: boolean,
33
41
  ): ISeparatorContext {
42
+ const {
43
+ firstId,
44
+ hasGlobal,
45
+ hasThis,
46
+ currentScope,
47
+ isStructParam,
48
+ isCppAccess,
49
+ forcePointerSemantics,
50
+ } = input;
34
51
  const isCrossScope =
35
52
  hasGlobal &&
36
53
  (deps.isKnownScope(firstId) || deps.isKnownRegister(firstId));
@@ -48,6 +65,7 @@ class MemberSeparatorResolver {
48
65
  isCppAccess,
49
66
  scopedRegName,
50
67
  isScopedRegister,
68
+ forcePointerSemantics,
51
69
  };
52
70
  }
53
71
 
@@ -66,7 +84,11 @@ class MemberSeparatorResolver {
66
84
  }
67
85
 
68
86
  // Struct parameter uses -> in C mode, . in C++ mode
87
+ // Issue #895: forcePointerSemantics overrides C++ mode to use ->
69
88
  if (ctx.isStructParam) {
89
+ if (ctx.forcePointerSemantics) {
90
+ return "->";
91
+ }
70
92
  return deps.getStructParamSeparator();
71
93
  }
72
94
 
@@ -28,6 +28,12 @@ class ParameterDereferenceResolver {
28
28
  paramInfo: TParameterInfo,
29
29
  deps: IParameterDereferenceDeps,
30
30
  ): boolean {
31
+ // Issue #895: Primitive params that became pointers due to callback typedef
32
+ // are NOT pass-by-value - they need dereferencing when used as values
33
+ if (paramInfo.isCallbackPointerPrimitive) {
34
+ return false; // Not pass-by-value → will be dereferenced
35
+ }
36
+
31
37
  // ADR-029: Callback parameters are function pointers
32
38
  if (paramInfo.isCallback) {
33
39
  return true;
@@ -92,6 +98,12 @@ class ParameterDereferenceResolver {
92
98
  return id;
93
99
  }
94
100
 
101
+ // Issue #895: Callback-compatible primitives always need dereferencing,
102
+ // even in C++ mode (because they use pointer semantics to match C typedef)
103
+ if (paramInfo.forcePointerSemantics) {
104
+ return `(*${id})`;
105
+ }
106
+
95
107
  // Known primitive that is pass-by-reference needs dereference
96
108
  // Issue #558/#644: In C++ mode, primitives become references
97
109
  return deps.maybeDereference(id);
@@ -42,6 +42,21 @@ interface IFromASTDeps {
42
42
 
43
43
  /** Whether the parameter should use pass-by-value (pre-computed) */
44
44
  isPassByValue: boolean;
45
+
46
+ /** Issue #895: Whether the current function is callback-compatible */
47
+ isCallbackCompatible: boolean;
48
+
49
+ /**
50
+ * Issue #895: Force pass-by-reference for callback-compatible functions
51
+ * When the typedef signature requires a pointer, this overrides normal logic.
52
+ */
53
+ forcePassByReference?: boolean;
54
+
55
+ /**
56
+ * Issue #895: Force const qualifier from callback typedef signature.
57
+ * When the C typedef has `const T*`, this preserves const on the generated param.
58
+ */
59
+ forceConst?: boolean;
45
60
  }
46
61
 
47
62
  /**
@@ -122,7 +137,15 @@ class ParameterInputAdapter {
122
137
  // Determine classification for non-array, non-string types
123
138
  const isKnownStruct = deps.isKnownStruct(typeName);
124
139
  const isKnownPrimitive = !!deps.typeMap[typeName];
125
- const isAutoConst = !deps.isModified && !isConst;
140
+ // Issue #895: Don't add auto-const for callback-compatible functions
141
+ // because it would change the signature and break typedef compatibility
142
+ const isAutoConst =
143
+ !deps.isCallbackCompatible && !deps.isModified && !isConst;
144
+
145
+ // Issue #895: For callback-compatible functions, force pass-by-reference
146
+ // when the typedef signature requires a pointer (e.g., opaque types)
147
+ const isPassByReference =
148
+ deps.forcePassByReference || isKnownStruct || isKnownPrimitive;
126
149
 
127
150
  return {
128
151
  name,
@@ -134,7 +157,12 @@ class ParameterInputAdapter {
134
157
  isCallback: false,
135
158
  isString: false,
136
159
  isPassByValue: deps.isPassByValue,
137
- isPassByReference: isKnownStruct || isKnownPrimitive,
160
+ isPassByReference,
161
+ // Issue #895: Force pointer syntax in C++ mode for callback-compatible functions
162
+ // because C callback typedefs expect pointers, not C++ references
163
+ forcePointerSyntax: deps.forcePassByReference,
164
+ // Issue #895: Preserve const from callback typedef signature
165
+ forceConst: deps.forceConst,
138
166
  };
139
167
  }
140
168
 
@@ -103,14 +103,19 @@ class ParameterSignatureBuilder {
103
103
  /**
104
104
  * Build pass-by-reference parameter signature.
105
105
  * C mode: const Point* p
106
- * C++ mode: const Point& p
106
+ * C++ mode: const Point& p (unless forcePointerSyntax)
107
+ *
108
+ * Issue #895: When forcePointerSyntax is set, always use pointer syntax
109
+ * because C callback typedefs expect pointers, not C++ references.
107
110
  */
108
111
  private static _buildRefParam(
109
112
  param: IParameterInput,
110
113
  refSuffix: string,
111
114
  ): string {
112
115
  const constPrefix = this._getConstPrefix(param);
113
- return `${constPrefix}${param.mappedType}${refSuffix} ${param.name}`;
116
+ // Issue #895: Override refSuffix for callback-compatible functions
117
+ const actualSuffix = param.forcePointerSyntax ? "*" : refSuffix;
118
+ return `${constPrefix}${param.mappedType}${actualSuffix} ${param.name}`;
114
119
  }
115
120
 
116
121
  /**
@@ -122,13 +127,16 @@ class ParameterSignatureBuilder {
122
127
  }
123
128
 
124
129
  /**
125
- * Get const prefix combining explicit const and auto-const.
126
- * Auto-const is applied to unmodified parameters.
130
+ * Get const prefix combining explicit const, auto-const, and forced const.
131
+ * Priority: forceConst > isConst > isAutoConst
132
+ * Issue #895: forceConst preserves const from callback typedef signature.
127
133
  */
128
134
  private static _getConstPrefix(param: IParameterInput): string {
129
- const autoConst = param.isAutoConst && !param.isConst ? "const " : "";
130
- const explicitConst = param.isConst ? "const " : "";
131
- return `${autoConst}${explicitConst}`;
135
+ // Any source of const results in "const " prefix
136
+ if (param.forceConst || param.isConst || param.isAutoConst) {
137
+ return "const ";
138
+ }
139
+ return "";
132
140
  }
133
141
  }
134
142
 
@@ -11,7 +11,7 @@
11
11
  import * as Parser from "../../../logic/parser/grammar/CNextParser.js";
12
12
  import CodeGenState from "../../../state/CodeGenState.js";
13
13
  import StringUtils from "../../../../utils/StringUtils.js";
14
- import ExpressionUnwrapper from "../utils/ExpressionUnwrapper.js";
14
+ import ExpressionUnwrapper from "../../../../utils/ExpressionUnwrapper";
15
15
 
16
16
  /** Regex for identifying valid C/C++ identifiers */
17
17
  const IDENTIFIER_REGEX = /^[a-zA-Z_]\w*$/;
@@ -0,0 +1,220 @@
1
+ /**
2
+ * TypedefParamParser - Parses C function pointer typedef signatures
3
+ *
4
+ * Extracts parameter types from typedef strings like:
5
+ * "void (*)(widget_t *, const rect_t *, uint8_t *)"
6
+ * "void (*)(Point p)"
7
+ *
8
+ * Used by Issue #895 to determine if callback params should be pointers or values.
9
+ */
10
+
11
+ /**
12
+ * Parsed parameter info from a typedef.
13
+ */
14
+ interface ITypedefParam {
15
+ /** Full type string (e.g., "widget_t *", "const rect_t *", "uint8_t *") */
16
+ type: string;
17
+ /** Whether this is a pointer type */
18
+ isPointer: boolean;
19
+ /** Whether this has const qualifier */
20
+ isConst: boolean;
21
+ /** Base type without pointer/const (e.g., "widget_t", "rect_t", "uint8_t") */
22
+ baseType: string;
23
+ }
24
+
25
+ /**
26
+ * Parse result for a typedef signature.
27
+ */
28
+ interface ITypedefParseResult {
29
+ /** Return type */
30
+ returnType: string;
31
+ /** Parsed parameters */
32
+ params: ITypedefParam[];
33
+ }
34
+
35
+ class TypedefParamParser {
36
+ /**
37
+ * Parse a function pointer typedef type string.
38
+ *
39
+ * @param typedefType - The type string, e.g., "void (*)(widget_t *, const rect_t *, uint8_t *)"
40
+ * @returns Parsed result with return type and parameters, or null if parsing fails
41
+ */
42
+ static parse(typedefType: string): ITypedefParseResult | null {
43
+ // Expected format: "return_type (*)(param1, param2, ...)"
44
+ // Find the (*) marker first
45
+ const funcPtrIndex = typedefType.indexOf("(*)");
46
+ if (funcPtrIndex === -1) {
47
+ return null;
48
+ }
49
+
50
+ const returnType = typedefType.substring(0, funcPtrIndex).trim();
51
+
52
+ // Find the opening paren after (*)
53
+ const afterFuncPtr = typedefType.substring(funcPtrIndex + 3).trim();
54
+ if (!afterFuncPtr.startsWith("(")) {
55
+ return null;
56
+ }
57
+
58
+ // Extract params by finding the matching closing paren
59
+ const paramsStr = TypedefParamParser.extractParenContent(afterFuncPtr);
60
+ if (paramsStr === null) {
61
+ return null;
62
+ }
63
+
64
+ // Handle void or empty params
65
+ if (!paramsStr || paramsStr === "void") {
66
+ return { returnType, params: [] };
67
+ }
68
+
69
+ // Split by comma, handling nested parentheses
70
+ const paramStrings = TypedefParamParser.splitParams(paramsStr);
71
+ const params = paramStrings.map((p) => TypedefParamParser.parseParam(p));
72
+
73
+ return { returnType, params };
74
+ }
75
+
76
+ /**
77
+ * Extract content between matching parentheses, handling arbitrary nesting.
78
+ * @param str - String starting with '('
79
+ * @returns Content between outer parens, or null if no match
80
+ */
81
+ private static extractParenContent(str: string): string | null {
82
+ if (!str.startsWith("(")) {
83
+ return null;
84
+ }
85
+
86
+ let depth = 0;
87
+ for (let i = 0; i < str.length; i++) {
88
+ if (str[i] === "(") {
89
+ depth++;
90
+ } else if (str[i] === ")") {
91
+ depth--;
92
+ if (depth === 0) {
93
+ // Found matching close paren - return content between
94
+ return str.substring(1, i);
95
+ }
96
+ }
97
+ }
98
+
99
+ // No matching close paren found
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Split parameter string by commas, respecting nested parentheses.
105
+ */
106
+ private static splitParams(paramsStr: string): string[] {
107
+ const params: string[] = [];
108
+ let current = "";
109
+ let depth = 0;
110
+
111
+ for (const char of paramsStr) {
112
+ if (char === "(") {
113
+ depth++;
114
+ current += char;
115
+ } else if (char === ")") {
116
+ depth--;
117
+ current += char;
118
+ } else if (char === "," && depth === 0) {
119
+ params.push(current.trim());
120
+ current = "";
121
+ } else {
122
+ current += char;
123
+ }
124
+ }
125
+
126
+ if (current.trim()) {
127
+ params.push(current.trim());
128
+ }
129
+
130
+ return params;
131
+ }
132
+
133
+ /**
134
+ * Parse a single parameter type string.
135
+ */
136
+ private static parseParam(paramStr: string): ITypedefParam {
137
+ const trimmed = paramStr.trim();
138
+
139
+ // Check for pointer
140
+ const isPointer = trimmed.includes("*");
141
+
142
+ // Check for const - handles both "const " (with space) and merged forms
143
+ // C grammar getText() strips spaces, so "const rect_t*" may appear merged
144
+ const isConst = /\bconst\b/.test(trimmed) || trimmed.startsWith("const");
145
+
146
+ // Extract base type (remove const, *, and param name if present)
147
+ let baseType = trimmed
148
+ .replaceAll(/\bconst\b/g, "") // Remove const (with word boundary)
149
+ .replace(/^const/, "") // Remove const at start (no space case) - only once
150
+ .replaceAll("*", "") // Remove pointers
151
+ .replaceAll(/\s+/g, " ") // Normalize whitespace
152
+ .trim();
153
+
154
+ // Remove trailing param name if present (e.g., "rect_t area" -> "rect_t")
155
+ // Only remove if there are multiple words (space-separated)
156
+ if (baseType.includes(" ")) {
157
+ baseType = baseType.replace(/\s+\w+$/, "");
158
+ }
159
+
160
+ // Handle struct keyword
161
+ if (baseType.startsWith("struct ")) {
162
+ baseType = baseType.substring(7);
163
+ }
164
+
165
+ return {
166
+ type: trimmed,
167
+ isPointer,
168
+ isConst,
169
+ baseType,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Get the parameter info at a given index, or null if not found.
175
+ */
176
+ private static getParamAt(
177
+ typedefType: string,
178
+ paramIndex: number,
179
+ ): ITypedefParam | null {
180
+ const parsed = TypedefParamParser.parse(typedefType);
181
+ if (!parsed || paramIndex >= parsed.params.length) {
182
+ return null;
183
+ }
184
+ return parsed.params[paramIndex];
185
+ }
186
+
187
+ /**
188
+ * Check if a parameter at a given index should be a pointer based on the typedef.
189
+ *
190
+ * @param typedefType - The typedef type string
191
+ * @param paramIndex - The parameter index (0-based)
192
+ * @returns true if the param should be a pointer, false for value, null if unknown
193
+ */
194
+ static shouldBePointer(
195
+ typedefType: string,
196
+ paramIndex: number,
197
+ ): boolean | null {
198
+ return (
199
+ TypedefParamParser.getParamAt(typedefType, paramIndex)?.isPointer ?? null
200
+ );
201
+ }
202
+
203
+ /**
204
+ * Check if a parameter at a given index should be const based on the typedef.
205
+ *
206
+ * @param typedefType - The typedef type string
207
+ * @param paramIndex - The parameter index (0-based)
208
+ * @returns true if the param should be const, false otherwise, null if unknown
209
+ */
210
+ static shouldBeConst(
211
+ typedefType: string,
212
+ paramIndex: number,
213
+ ): boolean | null {
214
+ return (
215
+ TypedefParamParser.getParamAt(typedefType, paramIndex)?.isConst ?? null
216
+ );
217
+ }
218
+ }
219
+
220
+ export default TypedefParamParser;