c-next 0.2.2 → 0.2.4

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 (48) hide show
  1. package/README.md +59 -57
  2. package/dist/index.js +641 -191
  3. package/dist/index.js.map +4 -4
  4. package/package.json +1 -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 +170 -14
  17. package/src/transpiler/logic/analysis/__tests__/ArrayIndexTypeAnalyzer.test.ts +545 -0
  18. package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +327 -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/index.ts +50 -1
  23. package/src/transpiler/logic/symbols/c/utils/DeclaratorUtils.ts +99 -2
  24. package/src/transpiler/logic/symbols/c/utils/__tests__/DeclaratorUtils.test.ts +128 -0
  25. package/src/transpiler/output/codegen/CodeGenerator.ts +31 -5
  26. package/src/transpiler/output/codegen/TypeValidator.ts +10 -7
  27. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +49 -36
  28. package/src/transpiler/output/codegen/__tests__/ExpressionWalker.test.ts +9 -3
  29. package/src/transpiler/output/codegen/__tests__/RequireInclude.test.ts +90 -25
  30. package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +3 -1
  31. package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +43 -29
  32. package/src/transpiler/output/codegen/generators/IOrchestrator.ts +5 -2
  33. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +23 -14
  34. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +10 -7
  35. package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +12 -3
  36. package/src/transpiler/output/codegen/generators/statements/SwitchGenerator.ts +10 -1
  37. package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +4 -4
  38. package/src/transpiler/output/codegen/helpers/ArrayAccessHelper.ts +6 -3
  39. package/src/transpiler/output/codegen/helpers/FloatBitHelper.ts +36 -22
  40. package/src/transpiler/output/codegen/helpers/FunctionContextManager.ts +9 -1
  41. package/src/transpiler/output/codegen/helpers/__tests__/ArrayAccessHelper.test.ts +8 -6
  42. package/src/transpiler/output/codegen/helpers/__tests__/FloatBitHelper.test.ts +34 -18
  43. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +5 -2
  44. package/src/transpiler/state/CodeGenState.ts +6 -0
  45. package/src/transpiler/types/IPipelineFile.ts +2 -2
  46. package/src/transpiler/types/IPipelineInput.ts +1 -1
  47. package/src/transpiler/types/TTranspileInput.ts +18 -0
  48. package/src/utils/constants/TypeConstants.ts +22 -0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Array Index Type Analyzer
3
+ * Detects signed and floating-point types used as array or bit subscript indexes
4
+ *
5
+ * C-Next requires unsigned integer types for all subscript operations to prevent
6
+ * undefined behavior from negative indexes. This analyzer catches type violations
7
+ * at compile time with clear error messages.
8
+ *
9
+ * Two-pass analysis:
10
+ * 1. Collect variable declarations with their types
11
+ * 2. Validate subscript expressions use unsigned integer types
12
+ *
13
+ * Uses CodeGenState for state-based type resolution (struct fields, function
14
+ * return types, enum detection) to handle complex expressions like arr[x + 1].
15
+ */
16
+
17
+ import { ParseTreeWalker } from "antlr4ng";
18
+ import { CNextListener } from "../parser/grammar/CNextListener";
19
+ import * as Parser from "../parser/grammar/CNextParser";
20
+ import IArrayIndexTypeError from "./types/IArrayIndexTypeError";
21
+ import LiteralUtils from "../../../utils/LiteralUtils";
22
+ import ParserUtils from "../../../utils/ParserUtils";
23
+ import TypeConstants from "../../../utils/constants/TypeConstants";
24
+ import CodeGenState from "../../state/CodeGenState";
25
+
26
+ /**
27
+ * First pass: Collect variable declarations with their types
28
+ */
29
+ class VariableTypeCollector extends CNextListener {
30
+ private readonly varTypes: Map<string, string> = new Map();
31
+
32
+ public getVarTypes(): Map<string, string> {
33
+ return this.varTypes;
34
+ }
35
+
36
+ private trackType(
37
+ typeCtx: Parser.TypeContext | null,
38
+ identifier: { getText(): string } | null,
39
+ ): void {
40
+ if (!typeCtx || !identifier) return;
41
+ this.varTypes.set(identifier.getText(), typeCtx.getText());
42
+ }
43
+
44
+ override enterVariableDeclaration = (
45
+ ctx: Parser.VariableDeclarationContext,
46
+ ): void => {
47
+ this.trackType(ctx.type(), ctx.IDENTIFIER());
48
+ };
49
+
50
+ override enterParameter = (ctx: Parser.ParameterContext): void => {
51
+ this.trackType(ctx.type(), ctx.IDENTIFIER());
52
+ };
53
+
54
+ override enterForVarDecl = (ctx: Parser.ForVarDeclContext): void => {
55
+ this.trackType(ctx.type(), ctx.IDENTIFIER());
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Second pass: Validate subscript index expressions use unsigned integer types
61
+ */
62
+ class IndexTypeListener extends CNextListener {
63
+ private readonly analyzer: ArrayIndexTypeAnalyzer;
64
+
65
+ // eslint-disable-next-line @typescript-eslint/lines-between-class-members
66
+ private readonly varTypes: Map<string, string>;
67
+
68
+ constructor(analyzer: ArrayIndexTypeAnalyzer, varTypes: Map<string, string>) {
69
+ super();
70
+ this.analyzer = analyzer;
71
+ this.varTypes = varTypes;
72
+ }
73
+
74
+ /**
75
+ * Check postfix operations in expressions (RHS: arr[idx], flags[bit])
76
+ */
77
+ override enterPostfixOp = (ctx: Parser.PostfixOpContext): void => {
78
+ if (!ctx.LBRACKET()) return;
79
+
80
+ const expressions = ctx.expression();
81
+ for (const expr of expressions) {
82
+ this.validateIndexExpression(expr);
83
+ }
84
+ };
85
+
86
+ /**
87
+ * Check postfix target operations in assignments (LHS: arr[idx] <- val)
88
+ */
89
+ override enterPostfixTargetOp = (
90
+ ctx: Parser.PostfixTargetOpContext,
91
+ ): void => {
92
+ if (!ctx.LBRACKET()) return;
93
+
94
+ const expressions = ctx.expression();
95
+ for (const expr of expressions) {
96
+ this.validateIndexExpression(expr);
97
+ }
98
+ };
99
+
100
+ /**
101
+ * Validate that a subscript index expression uses an unsigned integer type.
102
+ * Collects all leaf operands from the expression and checks each one.
103
+ */
104
+ private validateIndexExpression(ctx: Parser.ExpressionContext): void {
105
+ const operands = this.collectOperands(ctx);
106
+
107
+ for (const operand of operands) {
108
+ const resolvedType = this.resolveOperandType(operand);
109
+ if (!resolvedType) continue;
110
+
111
+ if (TypeConstants.SIGNED_TYPES.includes(resolvedType)) {
112
+ const { line, column } = ParserUtils.getPosition(ctx);
113
+ this.analyzer.addError(line, column, "E0850", resolvedType);
114
+ return;
115
+ }
116
+
117
+ if (
118
+ resolvedType === "float literal" ||
119
+ TypeConstants.FLOAT_TYPES.includes(resolvedType)
120
+ ) {
121
+ const { line, column } = ParserUtils.getPosition(ctx);
122
+ this.analyzer.addError(line, column, "E0851", resolvedType);
123
+ return;
124
+ }
125
+
126
+ if (TypeConstants.UNSIGNED_INDEX_TYPES.includes(resolvedType)) {
127
+ continue;
128
+ }
129
+
130
+ // Other non-integer types (e.g., string, struct) - E0852
131
+ const { line, column } = ParserUtils.getPosition(ctx);
132
+ this.analyzer.addError(line, column, "E0852", resolvedType);
133
+ return;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Collect all leaf unary expression operands from an expression tree.
139
+ * Handles binary operators at any level by flatMapping through the grammar hierarchy.
140
+ */
141
+ private collectOperands(
142
+ ctx: Parser.ExpressionContext,
143
+ ): Parser.UnaryExpressionContext[] {
144
+ const ternary = ctx.ternaryExpression();
145
+ if (!ternary) return [];
146
+
147
+ const orExpressions = ternary.orExpression();
148
+ if (orExpressions.length === 0) return [];
149
+
150
+ // For ternary (cond ? true : false), skip the condition (index 0)
151
+ // and only check the value branches (indices 1 and 2)
152
+ const valueExpressions =
153
+ orExpressions.length === 3 ? orExpressions.slice(1) : orExpressions;
154
+
155
+ return valueExpressions
156
+ .flatMap((o) => o.andExpression())
157
+ .flatMap((a) => a.equalityExpression())
158
+ .flatMap((e) => e.relationalExpression())
159
+ .flatMap((r) => r.bitwiseOrExpression())
160
+ .flatMap((bo) => bo.bitwiseXorExpression())
161
+ .flatMap((bx) => bx.bitwiseAndExpression())
162
+ .flatMap((ba) => ba.shiftExpression())
163
+ .flatMap((s) => s.additiveExpression())
164
+ .flatMap((a) => a.multiplicativeExpression())
165
+ .flatMap((m) => m.unaryExpression());
166
+ }
167
+
168
+ /**
169
+ * Resolve the type of a unary expression operand.
170
+ * Uses local varTypes first, then falls back to CodeGenState for
171
+ * struct fields, function return types, and enum detection.
172
+ *
173
+ * Returns null if the type cannot be resolved (pass-through).
174
+ */
175
+ private resolveOperandType(
176
+ operand: Parser.UnaryExpressionContext,
177
+ ): string | null {
178
+ const postfixExpr = operand.postfixExpression();
179
+ if (!postfixExpr) return null;
180
+
181
+ const primaryExpr = postfixExpr.primaryExpression();
182
+ if (!primaryExpr) return null;
183
+
184
+ // Resolve base type from primaryExpression
185
+ let currentType = this.resolveBaseType(primaryExpr);
186
+
187
+ // Walk postfix operators to transform the type
188
+ const postfixOps = postfixExpr.postfixOp();
189
+
190
+ // If base type is null but there are postfix ops, use identifier name
191
+ // for function call / member access resolution (e.g., getIndex())
192
+ if (!currentType && postfixOps.length > 0) {
193
+ const identifier = primaryExpr.IDENTIFIER();
194
+ if (identifier) {
195
+ currentType = identifier.getText();
196
+ }
197
+ }
198
+
199
+ for (const op of postfixOps) {
200
+ if (!currentType) return null;
201
+ currentType = this.resolvePostfixOpType(currentType, op);
202
+ }
203
+
204
+ return currentType;
205
+ }
206
+
207
+ /**
208
+ * Resolve the base type of a primary expression.
209
+ */
210
+ private resolveBaseType(
211
+ primaryExpr: Parser.PrimaryExpressionContext,
212
+ ): string | null {
213
+ // Check for literal
214
+ const literal = primaryExpr.literal();
215
+ if (literal) {
216
+ if (LiteralUtils.isFloat(literal)) return "float literal";
217
+ // Integer literals are always valid
218
+ return null;
219
+ }
220
+
221
+ // Check for parenthesized expression — recurse
222
+ const parenExpr = primaryExpr.expression();
223
+ if (parenExpr) {
224
+ const innerOperands = this.collectOperands(parenExpr);
225
+ for (const innerOp of innerOperands) {
226
+ const innerType = this.resolveOperandType(innerOp);
227
+ if (innerType) return innerType;
228
+ }
229
+ return null;
230
+ }
231
+
232
+ // Check for identifier
233
+ const identifier = primaryExpr.IDENTIFIER();
234
+ if (!identifier) return null;
235
+
236
+ const varName = identifier.getText();
237
+
238
+ // Local variables first (params, for-loop vars, function body vars)
239
+ const localType = this.varTypes.get(varName);
240
+ if (localType) return localType;
241
+
242
+ // Fall back to CodeGenState for cross-file variables
243
+ const typeInfo = CodeGenState.getVariableTypeInfo(varName);
244
+ if (typeInfo) return typeInfo.baseType;
245
+
246
+ return null;
247
+ }
248
+
249
+ /**
250
+ * Resolve the resulting type after applying a postfix operator.
251
+ */
252
+ private resolvePostfixOpType(
253
+ currentType: string,
254
+ op: Parser.PostfixOpContext,
255
+ ): string | null {
256
+ // Dot access (e.g., config.value, EColor.RED)
257
+ if (op.DOT()) {
258
+ const fieldId = op.IDENTIFIER();
259
+ if (!fieldId) return null;
260
+ const fieldName = fieldId.getText();
261
+
262
+ // Check if it's an enum access — always valid
263
+ if (CodeGenState.isKnownEnum(currentType)) return null;
264
+
265
+ // Check struct field type
266
+ const fieldType = CodeGenState.getStructFieldType(currentType, fieldName);
267
+ return fieldType ?? null;
268
+ }
269
+
270
+ // Array/bit subscript (e.g., lookup[idx])
271
+ if (op.LBRACKET()) {
272
+ // If current type is an array, result is the element type
273
+ // If current type is an integer, result is "bool" (bit access)
274
+ if (TypeConstants.UNSIGNED_INDEX_TYPES.includes(currentType)) {
275
+ return "bool";
276
+ }
277
+ if (TypeConstants.SIGNED_TYPES.includes(currentType)) {
278
+ return "bool";
279
+ }
280
+ // Array element type — strip array suffix
281
+ return currentType;
282
+ }
283
+
284
+ // Function call (e.g., getIndex())
285
+ if (op.LPAREN()) {
286
+ const returnType = CodeGenState.getFunctionReturnType(currentType);
287
+ return returnType ?? null;
288
+ }
289
+
290
+ return null;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Analyzer that detects non-unsigned-integer types used as subscript indexes
296
+ */
297
+ class ArrayIndexTypeAnalyzer {
298
+ private errors: IArrayIndexTypeError[] = [];
299
+
300
+ /**
301
+ * Analyze the parse tree for invalid subscript index types
302
+ */
303
+ public analyze(tree: Parser.ProgramContext): IArrayIndexTypeError[] {
304
+ this.errors = [];
305
+
306
+ // First pass: collect variable types
307
+ const collector = new VariableTypeCollector();
308
+ ParseTreeWalker.DEFAULT.walk(collector, tree);
309
+ const varTypes = collector.getVarTypes();
310
+
311
+ // Second pass: validate subscript index expressions
312
+ const listener = new IndexTypeListener(this, varTypes);
313
+ ParseTreeWalker.DEFAULT.walk(listener, tree);
314
+
315
+ return this.errors;
316
+ }
317
+
318
+ /**
319
+ * Add an index type error
320
+ */
321
+ public addError(
322
+ line: number,
323
+ column: number,
324
+ code: string,
325
+ actualType: string,
326
+ ): void {
327
+ this.errors.push({
328
+ code,
329
+ line,
330
+ column,
331
+ actualType,
332
+ message: `Subscript index must be an unsigned integer type; got '${actualType}'`,
333
+ helpText:
334
+ "Use an unsigned integer type (u8, u16, u32, u64) for array and bit subscript indexes.",
335
+ });
336
+ }
337
+
338
+ /**
339
+ * Get all detected errors
340
+ */
341
+ public getErrors(): IArrayIndexTypeError[] {
342
+ return this.errors;
343
+ }
344
+ }
345
+
346
+ export default ArrayIndexTypeAnalyzer;
@@ -13,6 +13,7 @@ import * as Parser from "../parser/grammar/CNextParser";
13
13
  import SymbolTable from "../symbols/SymbolTable";
14
14
  import IFunctionCallError from "./types/IFunctionCallError";
15
15
  import ParserUtils from "../../../utils/ParserUtils";
16
+ import CodeGenState from "../../state/CodeGenState";
16
17
 
17
18
  /**
18
19
  * C-Next built-in functions
@@ -215,6 +216,17 @@ class FunctionCallListener extends CNextListener {
215
216
  // ISR/Callback Variable Tracking (ADR-040)
216
217
  // ========================================================================
217
218
 
219
+ /**
220
+ * Check if a type name represents a callable type (ISR, callback, or C function pointer typedef).
221
+ */
222
+ private isCallableType(typeName: string): boolean {
223
+ return (
224
+ typeName === "ISR" ||
225
+ this.analyzer.isCallbackType(typeName) ||
226
+ this.analyzer.isCFunctionPointerTypedef(typeName)
227
+ );
228
+ }
229
+
218
230
  /**
219
231
  * Track ISR-typed variables from variable declarations
220
232
  * e.g., `ISR handler <- myFunction;`
@@ -222,13 +234,9 @@ class FunctionCallListener extends CNextListener {
222
234
  override enterVariableDeclaration = (
223
235
  ctx: Parser.VariableDeclarationContext,
224
236
  ): void => {
225
- const typeCtx = ctx.type();
226
- const typeName = typeCtx.getText();
227
-
228
- // Check if this is an ISR type or a callback type (function-as-type)
229
- if (typeName === "ISR" || this.analyzer.isCallbackType(typeName)) {
230
- const varName = ctx.IDENTIFIER().getText();
231
- this.analyzer.defineCallableVariable(varName);
237
+ const typeName = ctx.type().getText();
238
+ if (this.isCallableType(typeName)) {
239
+ this.analyzer.defineCallableVariable(ctx.IDENTIFIER().getText());
232
240
  }
233
241
  };
234
242
 
@@ -237,13 +245,9 @@ class FunctionCallListener extends CNextListener {
237
245
  * e.g., `void execute(ISR handler) { handler(); }`
238
246
  */
239
247
  override enterParameter = (ctx: Parser.ParameterContext): void => {
240
- const typeCtx = ctx.type();
241
- const typeName = typeCtx.getText();
242
-
243
- // Check if this is an ISR type or a callback type (function-as-type)
244
- if (typeName === "ISR" || this.analyzer.isCallbackType(typeName)) {
245
- const paramName = ctx.IDENTIFIER().getText();
246
- this.analyzer.defineCallableVariable(paramName);
248
+ const typeName = ctx.type().getText();
249
+ if (this.isCallableType(typeName)) {
250
+ this.analyzer.defineCallableVariable(ctx.IDENTIFIER().getText());
247
251
  }
248
252
  };
249
253
 
@@ -451,6 +455,7 @@ class FunctionCallAnalyzer {
451
455
  this.collectIncludes(tree);
452
456
  this.collectCallbackTypes(tree);
453
457
  this.collectAllLocalFunctions(tree);
458
+ this.collectCallbackCompatibleFunctions(tree);
454
459
 
455
460
  // Second pass: walk tree in order, tracking definitions and checking calls
456
461
  const listener = new FunctionCallListener(this);
@@ -551,6 +556,157 @@ class FunctionCallAnalyzer {
551
556
  return this.callbackTypes.has(name);
552
557
  }
553
558
 
559
+ /**
560
+ * Check if a type name is a C function pointer typedef.
561
+ * Looks up the type in the symbol table and checks if it's a typedef
562
+ * whose underlying type contains "(*)" indicating a function pointer.
563
+ */
564
+ public isCFunctionPointerTypedef(typeName: string): boolean {
565
+ if (!this.symbolTable) return false;
566
+ const sym = this.symbolTable.getCSymbol(typeName);
567
+ if (sym?.kind !== "type") return false;
568
+ // ICTypedefSymbol has a `type` field with the underlying C type string
569
+ return (
570
+ "type" in sym && typeof sym.type === "string" && sym.type.includes("(*)")
571
+ );
572
+ }
573
+
574
+ /**
575
+ * Detect functions assigned to C function pointer typedefs.
576
+ * When `PointCallback cb <- my_handler;` is found and PointCallback
577
+ * is a C function pointer typedef, mark my_handler as callback-compatible.
578
+ */
579
+ private collectCallbackCompatibleFunctions(
580
+ tree: Parser.ProgramContext,
581
+ ): void {
582
+ for (const decl of tree.declaration()) {
583
+ const funcDecl = decl.functionDeclaration();
584
+ if (!funcDecl) continue;
585
+
586
+ const block = funcDecl.block();
587
+ if (!block) continue;
588
+
589
+ this.scanBlockForCallbackAssignments(block);
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Recursively scan all statements in a block for callback typedef assignments.
595
+ */
596
+ private scanBlockForCallbackAssignments(block: Parser.BlockContext): void {
597
+ for (const stmt of block.statement()) {
598
+ this.scanStatementForCallbackAssignments(stmt);
599
+ }
600
+ }
601
+
602
+ /**
603
+ * Scan a single statement for callback typedef assignments,
604
+ * recursing into nested blocks (if/while/for/do-while/switch/critical).
605
+ */
606
+ private scanStatementForCallbackAssignments(
607
+ stmt: Parser.StatementContext,
608
+ ): void {
609
+ // Check variable declarations for callback assignments
610
+ const varDecl = stmt.variableDeclaration();
611
+ if (varDecl) {
612
+ this.checkVarDeclForCallbackAssignment(varDecl);
613
+ return;
614
+ }
615
+
616
+ // Recurse into nested blocks/statements
617
+ const ifStmt = stmt.ifStatement();
618
+ if (ifStmt) {
619
+ for (const child of ifStmt.statement()) {
620
+ this.scanStatementForCallbackAssignments(child);
621
+ }
622
+ return;
623
+ }
624
+
625
+ const whileStmt = stmt.whileStatement();
626
+ if (whileStmt) {
627
+ this.scanStatementForCallbackAssignments(whileStmt.statement());
628
+ return;
629
+ }
630
+
631
+ const forStmt = stmt.forStatement();
632
+ if (forStmt) {
633
+ this.scanStatementForCallbackAssignments(forStmt.statement());
634
+ return;
635
+ }
636
+
637
+ const doWhileStmt = stmt.doWhileStatement();
638
+ if (doWhileStmt) {
639
+ this.scanBlockForCallbackAssignments(doWhileStmt.block());
640
+ return;
641
+ }
642
+
643
+ const switchStmt = stmt.switchStatement();
644
+ if (switchStmt) {
645
+ for (const caseCtx of switchStmt.switchCase()) {
646
+ this.scanBlockForCallbackAssignments(caseCtx.block());
647
+ }
648
+ const defaultCtx = switchStmt.defaultCase();
649
+ if (defaultCtx) {
650
+ this.scanBlockForCallbackAssignments(defaultCtx.block());
651
+ }
652
+ return;
653
+ }
654
+
655
+ const criticalStmt = stmt.criticalStatement();
656
+ if (criticalStmt) {
657
+ this.scanBlockForCallbackAssignments(criticalStmt.block());
658
+ return;
659
+ }
660
+
661
+ // A statement can itself be a block
662
+ const nestedBlock = stmt.block();
663
+ if (nestedBlock) {
664
+ this.scanBlockForCallbackAssignments(nestedBlock);
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Check if a variable declaration assigns a function to a C callback typedef.
670
+ */
671
+ private checkVarDeclForCallbackAssignment(
672
+ varDecl: Parser.VariableDeclarationContext,
673
+ ): void {
674
+ const typeName = varDecl.type().getText();
675
+ if (!this.isCFunctionPointerTypedef(typeName)) return;
676
+
677
+ const expr = varDecl.expression();
678
+ if (!expr) return;
679
+
680
+ const funcRef = this.extractFunctionReference(expr);
681
+ if (!funcRef) return;
682
+
683
+ // Scope-qualified names use dot in source (MyScope.handler) but
684
+ // allLocalFunctions stores them with underscore (MyScope_handler)
685
+ const lookupName = funcRef.includes(".")
686
+ ? funcRef.replace(".", "_")
687
+ : funcRef;
688
+
689
+ if (this.allLocalFunctions.has(lookupName)) {
690
+ CodeGenState.callbackCompatibleFunctions.add(lookupName);
691
+ }
692
+ }
693
+
694
+ /**
695
+ * Extract a function reference from an expression context.
696
+ * Matches bare identifiers (e.g., "my_handler") and qualified scope
697
+ * names (e.g., "MyScope.handler").
698
+ * Returns null if the expression is not a function reference.
699
+ */
700
+ private extractFunctionReference(
701
+ expr: Parser.ExpressionContext,
702
+ ): string | null {
703
+ const text = expr.getText();
704
+ if (/^\w+(\.\w+)?$/.test(text)) {
705
+ return text;
706
+ }
707
+ return null;
708
+ }
709
+
554
710
  /**
555
711
  * ADR-040: Register a variable that holds a callable (ISR or callback)
556
712
  */