c-next 0.2.16 → 0.2.18

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 (118) hide show
  1. package/README.md +18 -2
  2. package/dist/index.js +8897 -6260
  3. package/dist/index.js.map +4 -4
  4. package/grammar/CNext.g4 +12 -0
  5. package/package.json +4 -2
  6. package/src/transpiler/Transpiler.ts +376 -48
  7. package/src/transpiler/__tests__/DualCodePaths.test.ts +1 -1
  8. package/src/transpiler/__tests__/compileCommandsDiscovery.integration.test.ts +94 -0
  9. package/src/transpiler/__tests__/externalSymbolRecovery.integration.test.ts +215 -0
  10. package/src/transpiler/logic/__tests__/detectAssemblySyntax.test.ts +116 -0
  11. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +65 -10
  12. package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +186 -14
  13. package/src/transpiler/logic/analysis/MixedTypeCategoryAnalyzer.ts +408 -0
  14. package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +16 -97
  15. package/src/transpiler/logic/analysis/ReturnPathAnalyzer.ts +171 -0
  16. package/src/transpiler/logic/analysis/SignedShiftAnalyzer.ts +124 -12
  17. package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +200 -0
  18. package/src/transpiler/logic/analysis/__tests__/InitializationAnalyzer.test.ts +386 -1
  19. package/src/transpiler/logic/analysis/__tests__/MixedTypeCategoryAnalyzer.test.ts +346 -0
  20. package/src/transpiler/logic/analysis/__tests__/ReturnPathAnalyzer.test.ts +232 -0
  21. package/src/transpiler/logic/analysis/__tests__/SignedShiftAnalyzer.test.ts +211 -0
  22. package/src/transpiler/logic/analysis/runAnalyzers.ts +23 -2
  23. package/src/transpiler/logic/analysis/types/IMixedTypeCategoryError.ts +17 -0
  24. package/src/transpiler/logic/analysis/types/IReturnPathError.ts +12 -0
  25. package/src/transpiler/logic/detectAssemblySyntax.ts +80 -0
  26. package/src/transpiler/logic/parser/grammar/CNext.interp +4 -1
  27. package/src/transpiler/logic/parser/grammar/CNext.tokens +169 -167
  28. package/src/transpiler/logic/parser/grammar/CNextLexer.interp +4 -1
  29. package/src/transpiler/logic/parser/grammar/CNextLexer.tokens +169 -167
  30. package/src/transpiler/logic/parser/grammar/CNextLexer.ts +545 -541
  31. package/src/transpiler/logic/parser/grammar/CNextListener.ts +11 -0
  32. package/src/transpiler/logic/parser/grammar/CNextParser.ts +1318 -1178
  33. package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +7 -0
  34. package/src/transpiler/logic/preprocessor/CompileCommandsReader.ts +332 -0
  35. package/src/transpiler/logic/preprocessor/ExternalDeclarationOracle.ts +202 -0
  36. package/src/transpiler/logic/preprocessor/Preprocessor.ts +24 -6
  37. package/src/transpiler/logic/preprocessor/ToolchainDetector.ts +42 -1
  38. package/src/transpiler/logic/preprocessor/__tests__/CompileCommandsReader.test.ts +216 -0
  39. package/src/transpiler/logic/preprocessor/__tests__/ExternalDeclarationOracle.test.ts +153 -0
  40. package/src/transpiler/logic/preprocessor/__tests__/Preprocessor.test.ts +43 -18
  41. package/src/transpiler/logic/preprocessor/__tests__/ToolchainDetector.crossCompiler.test.ts +75 -0
  42. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/dependent.h +7 -0
  43. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/predecessor.h +5 -0
  44. package/src/transpiler/logic/preprocessor/types/ICompileCommandsResult.ts +14 -0
  45. package/src/transpiler/logic/preprocessor/types/IPreprocessOptions.ts +17 -0
  46. package/src/transpiler/logic/symbols/SymbolTable.ts +62 -2
  47. package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +55 -4
  48. package/src/transpiler/logic/symbols/cnext/collectors/VariableCollector.ts +15 -2
  49. package/src/transpiler/logic/symbols/cnext/utils/TypeUtils.ts +64 -50
  50. package/src/transpiler/output/MisraSuppressionUtils.ts +52 -0
  51. package/src/transpiler/output/__tests__/MisraSuppressionUtils.test.ts +67 -0
  52. package/src/transpiler/output/codegen/CodeGenerator.ts +196 -98
  53. package/src/transpiler/output/codegen/TypeResolver.ts +148 -0
  54. package/src/transpiler/output/codegen/TypeValidator.ts +179 -81
  55. package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +165 -17
  56. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +163 -35
  57. package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +2 -2
  58. package/src/transpiler/output/codegen/__tests__/TypeResolver.test.ts +93 -0
  59. package/src/transpiler/output/codegen/__tests__/TypeValidator.alwaysTrueLoop.test.ts +91 -0
  60. package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +97 -14
  61. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +13 -0
  62. package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +1 -1
  63. package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +20 -12
  64. package/src/transpiler/output/codegen/assignment/handlers/ArrayHandlers.ts +424 -22
  65. package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +31 -18
  66. package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +7 -8
  67. package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +6 -8
  68. package/src/transpiler/output/codegen/assignment/handlers/RegisterUtils.ts +12 -10
  69. package/src/transpiler/output/codegen/assignment/handlers/SimpleHandler.ts +3 -7
  70. package/src/transpiler/output/codegen/assignment/handlers/SpecialHandlers.ts +7 -9
  71. package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +12 -10
  72. package/src/transpiler/output/codegen/assignment/handlers/__tests__/ArrayHandlers.test.ts +479 -11
  73. package/src/transpiler/output/codegen/generators/IOrchestrator.ts +3 -0
  74. package/src/transpiler/output/codegen/generators/declarationGenerators/ScopeGenerator.ts +26 -7
  75. package/src/transpiler/output/codegen/generators/declarationGenerators/__tests__/ScopeGenerator.test.ts +86 -0
  76. package/src/transpiler/output/codegen/generators/expressions/BinaryExprGenerator.ts +43 -24
  77. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +76 -58
  78. package/src/transpiler/output/codegen/generators/expressions/ExpressionGenerator.ts +9 -2
  79. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +26 -13
  80. package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +44 -0
  81. package/src/transpiler/output/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts +82 -1
  82. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +129 -1
  83. package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +64 -8
  84. package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +8 -5
  85. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +30 -2
  86. package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +17 -3
  87. package/src/transpiler/output/codegen/helpers/ParameterSignatureBuilder.ts +17 -4
  88. package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +240 -42
  89. package/src/transpiler/output/codegen/helpers/SymbolLookupHelper.ts +0 -21
  90. package/src/transpiler/output/codegen/helpers/TypeGenerationHelper.ts +60 -39
  91. package/src/transpiler/output/codegen/helpers/TypeRegistrationEngine.ts +170 -36
  92. package/src/transpiler/output/codegen/helpers/VariableDeclHelper.ts +37 -39
  93. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +19 -0
  94. package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +117 -0
  95. package/src/transpiler/output/codegen/helpers/__tests__/ParameterSignatureBuilder.test.ts +94 -2
  96. package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +322 -9
  97. package/src/transpiler/output/codegen/helpers/__tests__/SymbolLookupHelper.test.ts +0 -64
  98. package/src/transpiler/output/codegen/helpers/__tests__/TypeRegistrationEngine.test.ts +101 -0
  99. package/src/transpiler/output/codegen/helpers/__tests__/VariableDeclHelper.test.ts +29 -5
  100. package/src/transpiler/output/codegen/subscript/SubscriptClassifier.ts +30 -11
  101. package/src/transpiler/output/codegen/subscript/TSubscriptKind.ts +1 -1
  102. package/src/transpiler/output/codegen/subscript/__tests__/SubscriptClassifier.test.ts +38 -6
  103. package/src/transpiler/output/codegen/types/ICallbackTypeInfo.ts +2 -1
  104. package/src/transpiler/output/codegen/types/IParameterInput.ts +7 -0
  105. package/src/transpiler/output/headers/BaseHeaderGenerator.ts +8 -0
  106. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +140 -24
  107. package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +280 -0
  108. package/src/transpiler/output/headers/generators/IHeaderTypeInput.ts +13 -0
  109. package/src/transpiler/output/headers/generators/generateStructHeader.ts +48 -28
  110. package/src/transpiler/state/CodeGenState.ts +91 -22
  111. package/src/transpiler/state/__tests__/CodeGenState.test.ts +253 -11
  112. package/src/transpiler/types/ICachedFileEntry.ts +7 -0
  113. package/src/utils/LiteralUtils.ts +23 -0
  114. package/src/utils/__tests__/LiteralUtils.test.ts +101 -0
  115. package/src/utils/cache/CacheManager.ts +13 -2
  116. package/src/utils/cache/__tests__/CacheManager.test.ts +18 -1
  117. package/src/utils/constants/TypeConstants.ts +13 -0
  118. package/src/utils/types/IParameterSymbol.ts +1 -0
@@ -157,10 +157,32 @@ class InitializationListener extends CNextListener {
157
157
  return;
158
158
  }
159
159
 
160
- // Simple variable assignment: x <- value (no postfix ops)
160
+ // Resolve the assignment target ONCE (single source of truth)
161
+ const target = this._resolveAssignmentTarget(baseId, postfixOps);
162
+
163
+ // Issue #1012: Compound assignment reads the LHS before writing.
164
+ // Check if the assignment operator is a compound operator (not simple `<-`).
165
+ const isCompoundAssignment = ctx.assignmentOperator().ASSIGN() === null;
166
+ if (isCompoundAssignment) {
167
+ const { line, column } = ParserUtils.getPosition(ctx);
168
+ this.analyzer.checkRead(target.varName, line, column, target.fieldName);
169
+ }
170
+
171
+ // Record the assignment to the resolved target
172
+ this.analyzer.recordAssignment(target.varName, target.fieldName);
173
+ };
174
+
175
+ /**
176
+ * Resolve an assignment target to its variable name and optional field name.
177
+ * Single classification path used by both read checks and assignment recording.
178
+ */
179
+ private _resolveAssignmentTarget(
180
+ baseId: string,
181
+ postfixOps: Parser.PostfixTargetOpContext[],
182
+ ): { varName: string; fieldName?: string } {
183
+ // Simple variable: x <- value (no postfix ops)
161
184
  if (postfixOps.length === 0) {
162
- this.analyzer.recordAssignment(baseId);
163
- return;
185
+ return { varName: baseId };
164
186
  }
165
187
 
166
188
  // Analyze postfix operations
@@ -168,15 +190,13 @@ class InitializationListener extends CNextListener {
168
190
 
169
191
  // Member access: p.x <- value (struct field)
170
192
  if (identifiers.length >= 2 && !hasSubscript) {
171
- const varName = identifiers[0];
172
- const fieldName = identifiers[1];
173
- this.analyzer.recordAssignment(varName, fieldName);
174
- } else {
175
- // Array access or mixed: arr[i] <- value or arr[i].field <- value
176
- // Consider the array/base as a whole initialized
177
- this.analyzer.recordAssignment(baseId);
193
+ return { varName: identifiers[0], fieldName: identifiers[1] };
178
194
  }
179
- };
195
+
196
+ // Array access or mixed: arr[i] <- value or arr[i].field <- value
197
+ // Consider the array/base as a whole
198
+ return { varName: baseId };
199
+ }
180
200
 
181
201
  // ========================================================================
182
202
  // Function Call Arguments (ADR-006: pass-by-reference may initialize)
@@ -526,14 +546,159 @@ class InitializationAnalyzer {
526
546
 
527
547
  /**
528
548
  * Process scope member variable declarations (ADR-016)
549
+ * Issue #1019: Scope members require explicit initialization like locals
529
550
  */
530
551
  private _processScopeMembers(decl: Parser.DeclarationContext): void {
531
552
  const scopeDecl = decl.scopeDeclaration();
532
553
  if (!scopeDecl) return;
533
554
 
534
555
  const scopeName = scopeDecl.IDENTIFIER().getText();
556
+
557
+ // Phase 1: Find all members assigned in any scope function
558
+ const assignedMembers = this._findAssignedScopeMembers(scopeDecl);
559
+
560
+ // Phase 2: Process each member with assignment info
561
+ for (const member of scopeDecl.scopeMember()) {
562
+ this._processScopeMemberVariable(member, scopeName, assignedMembers);
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Scan all functions in a scope to find which members are assigned.
568
+ * Issue #1019: A member assigned in ANY function is considered initialized
569
+ * for reads in other functions within the same scope.
570
+ */
571
+ private _findAssignedScopeMembers(
572
+ scopeDecl: Parser.ScopeDeclarationContext,
573
+ ): Set<string> {
574
+ const assigned = new Set<string>();
575
+
535
576
  for (const member of scopeDecl.scopeMember()) {
536
- this._processScopeMemberVariable(member, scopeName);
577
+ const funcDecl = member.functionDeclaration();
578
+ if (!funcDecl) continue;
579
+
580
+ const body = funcDecl.block();
581
+ if (!body) continue;
582
+
583
+ // Scan the function body for assignments to scope members
584
+ this._collectAssignmentsInBlock(body, assigned);
585
+ }
586
+
587
+ return assigned;
588
+ }
589
+
590
+ /**
591
+ * Recursively collect variable names that are assigned in a block.
592
+ * Looks for assignment statements targeting bare identifiers or this.member.
593
+ */
594
+ private _collectAssignmentsInBlock(
595
+ block: Parser.BlockContext,
596
+ assigned: Set<string>,
597
+ ): void {
598
+ for (const stmt of block.statement()) {
599
+ this._collectAssignmentsInStatement(stmt, assigned);
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Collect assignments from a single statement, recursing into nested blocks.
605
+ */
606
+ private _collectAssignmentsInStatement(
607
+ stmt: Parser.StatementContext,
608
+ assigned: Set<string>,
609
+ ): void {
610
+ this._collectDirectAssignment(stmt, assigned);
611
+ this._collectFromControlFlow(stmt, assigned);
612
+ this._collectFromSwitch(stmt, assigned);
613
+ this._collectFromBlock(stmt, assigned);
614
+ }
615
+
616
+ /**
617
+ * Collect assignment from the statement itself (if it's an assignment).
618
+ */
619
+ private _collectDirectAssignment(
620
+ stmt: Parser.StatementContext,
621
+ assigned: Set<string>,
622
+ ): void {
623
+ const assignStmt = stmt.assignmentStatement();
624
+ if (!assignStmt) return;
625
+
626
+ const target = assignStmt.assignmentTarget();
627
+ if (!target) return;
628
+
629
+ // Both bare identifier and this.member use the same IDENTIFIER token
630
+ const id = target.IDENTIFIER()?.getText();
631
+ if (id) {
632
+ assigned.add(id);
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Recurse into control flow statements (if, while, do-while, for).
638
+ */
639
+ private _collectFromControlFlow(
640
+ stmt: Parser.StatementContext,
641
+ assigned: Set<string>,
642
+ ): void {
643
+ // if statement
644
+ const ifStmt = stmt.ifStatement();
645
+ if (ifStmt) {
646
+ for (const childStmt of ifStmt.statement()) {
647
+ this._collectAssignmentsInStatement(childStmt, assigned);
648
+ }
649
+ }
650
+
651
+ // while statement
652
+ const whileBody = stmt.whileStatement()?.statement();
653
+ if (whileBody) {
654
+ this._collectAssignmentsInStatement(whileBody, assigned);
655
+ }
656
+
657
+ // do-while statement (Issue #1019 review feedback)
658
+ const doWhileBody = stmt.doWhileStatement()?.block();
659
+ if (doWhileBody) {
660
+ this._collectAssignmentsInBlock(doWhileBody, assigned);
661
+ }
662
+
663
+ // for statement
664
+ const forBody = stmt.forStatement()?.statement();
665
+ if (forBody) {
666
+ this._collectAssignmentsInStatement(forBody, assigned);
667
+ }
668
+ }
669
+
670
+ /**
671
+ * Recurse into switch statement cases.
672
+ */
673
+ private _collectFromSwitch(
674
+ stmt: Parser.StatementContext,
675
+ assigned: Set<string>,
676
+ ): void {
677
+ const switchStmt = stmt.switchStatement();
678
+ if (!switchStmt) return;
679
+
680
+ for (const switchCase of switchStmt.switchCase()) {
681
+ const caseBlock = switchCase.block();
682
+ if (caseBlock) {
683
+ this._collectAssignmentsInBlock(caseBlock, assigned);
684
+ }
685
+ }
686
+ const defaultBlock = switchStmt.defaultCase()?.block();
687
+ if (defaultBlock) {
688
+ this._collectAssignmentsInBlock(defaultBlock, assigned);
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Recurse into standalone block statement.
694
+ */
695
+ private _collectFromBlock(
696
+ stmt: Parser.StatementContext,
697
+ assigned: Set<string>,
698
+ ): void {
699
+ const block = stmt.block();
700
+ if (block) {
701
+ this._collectAssignmentsInBlock(block, assigned);
537
702
  }
538
703
  }
539
704
 
@@ -543,6 +708,7 @@ class InitializationAnalyzer {
543
708
  private _processScopeMemberVariable(
544
709
  member: Parser.ScopeMemberContext,
545
710
  scopeName: string,
711
+ assignedMembers: Set<string>,
546
712
  ): void {
547
713
  const memberVar = member.variableDeclaration();
548
714
  if (!memberVar) return;
@@ -552,9 +718,15 @@ class InitializationAnalyzer {
552
718
  const { line, column } = ParserUtils.getPosition(memberVar);
553
719
  const typeName = this._extractUserTypeName(memberVar.type());
554
720
 
721
+ // Issue #1019: Scope members are initialized if they have an inline
722
+ // initializer OR are assigned in any function within the scope
723
+ const hasInlineInit = memberVar.expression() !== null;
724
+ const isAssignedInScope = assignedMembers.has(varName);
725
+ const hasInitializer = hasInlineInit || isAssignedInScope;
726
+
555
727
  // Register with both raw name and transpiled C name for scope resolution
556
- this.declareVariable(varName, line, column, true, typeName);
557
- this.declareVariable(fullName, line, column, true, typeName);
728
+ this.declareVariable(varName, line, column, hasInitializer, typeName);
729
+ this.declareVariable(fullName, line, column, hasInitializer, typeName);
558
730
  }
559
731
 
560
732
  /**
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Mixed Type Category Analyzer
3
+ *
4
+ * Detects binary operators whose two operands have different essential type
5
+ * categories (signed vs unsigned) at compile time.
6
+ *
7
+ * MISRA C:2012 Rule 10.4: "Both operands of an operator in which the usual
8
+ * arithmetic conversions are performed shall have the same essential type
9
+ * category." Combining a signed and an unsigned value (e.g. `u32 + i32`) relies
10
+ * on C's usual arithmetic conversions, whose result can be surprising (ADR-024).
11
+ *
12
+ * To combine values of different categories, the developer reinterprets one
13
+ * operand's bits to the other's category with bit indexing (ADR-007), e.g.
14
+ * `a + b[0, 32]`, making the conversion explicit.
15
+ *
16
+ * Integer literals are exempt: a bare literal has no fixed essential category —
17
+ * it is contextually typed to the other operand (ADR-052), so `a + 5` is fine.
18
+ * The rule fires only when BOTH operands resolve to concrete, fixed-width
19
+ * integer types of different category.
20
+ *
21
+ * Two-pass analysis:
22
+ * 1. Collect declarations into per-scope frames (function, named scope, block,
23
+ * and for-loop header), so a name is resolved against ITS scope — a same-named
24
+ * variable of a different category in another function OR a nested block never
25
+ * poisons the lookup (Issue #1085 review).
26
+ * 2. Walk each binary-operator level and compare adjacent operand categories,
27
+ * resolving each operand within its enclosing scope frame.
28
+ *
29
+ * Note: shift operators (<< / >>) are intentionally NOT checked here — MISRA
30
+ * Rule 10.4 only governs operators subject to the usual arithmetic conversions,
31
+ * and a shift count is promoted independently. A signed shift count is a Rule
32
+ * 10.1 concern handled elsewhere (Issue #1085 review).
33
+ */
34
+
35
+ import { ParseTreeWalker, ParserRuleContext } from "antlr4ng";
36
+ import { CNextListener } from "../parser/grammar/CNextListener";
37
+ import * as Parser from "../parser/grammar/CNextParser";
38
+ import IMixedTypeCategoryError from "./types/IMixedTypeCategoryError";
39
+ import ParserUtils from "../../../utils/ParserUtils";
40
+ import TypeConstants from "../../../utils/constants/TypeConstants";
41
+
42
+ /** Essential type category of an operand, or null when it cannot be resolved. */
43
+ type Category = "signed" | "unsigned" | null;
44
+
45
+ /**
46
+ * Declarations directly in one lexical scope (a function, named scope, block, or
47
+ * for-loop header), with a link to the enclosing scope. Resolution searches
48
+ * outward to the global frame, so inner declarations shadow outer ones.
49
+ */
50
+ interface ScopeFrame {
51
+ readonly vars: Map<string, string>;
52
+ readonly parent: ScopeFrame | null;
53
+ }
54
+
55
+ /**
56
+ * First pass: build per-scope frames. Frames are anchored to the function /
57
+ * scope context node so the second pass can find an operand's frame by walking
58
+ * up its parent chain — no shared walk state between the passes.
59
+ */
60
+ class ScopeCollector extends CNextListener {
61
+ private readonly globalFrame: ScopeFrame = { vars: new Map(), parent: null };
62
+
63
+ // eslint-disable-next-line @typescript-eslint/lines-between-class-members
64
+ private readonly frameOf: Map<ParserRuleContext, ScopeFrame> = new Map();
65
+
66
+ // eslint-disable-next-line @typescript-eslint/lines-between-class-members
67
+ private readonly stack: ScopeFrame[] = [this.globalFrame];
68
+
69
+ public getGlobalFrame(): ScopeFrame {
70
+ return this.globalFrame;
71
+ }
72
+
73
+ public getFrameOf(): Map<ParserRuleContext, ScopeFrame> {
74
+ return this.frameOf;
75
+ }
76
+
77
+ private top(): ScopeFrame {
78
+ return this.stack.at(-1) ?? this.globalFrame;
79
+ }
80
+
81
+ private pushFrame(node: ParserRuleContext): void {
82
+ const frame: ScopeFrame = { vars: new Map(), parent: this.top() };
83
+ this.frameOf.set(node, frame);
84
+ this.stack.push(frame);
85
+ }
86
+
87
+ private popFrame(): void {
88
+ this.stack.pop();
89
+ }
90
+
91
+ private record(
92
+ typeCtx: Parser.TypeContext | null,
93
+ identifier: { getText(): string } | null,
94
+ ): void {
95
+ if (!typeCtx || !identifier) return;
96
+ this.top().vars.set(identifier.getText(), typeCtx.getText());
97
+ }
98
+
99
+ override enterFunctionDeclaration = (
100
+ ctx: Parser.FunctionDeclarationContext,
101
+ ): void => {
102
+ this.pushFrame(ctx);
103
+ };
104
+
105
+ override exitFunctionDeclaration = (): void => {
106
+ this.popFrame();
107
+ };
108
+
109
+ override enterScopeDeclaration = (
110
+ ctx: Parser.ScopeDeclarationContext,
111
+ ): void => {
112
+ this.pushFrame(ctx);
113
+ };
114
+
115
+ override exitScopeDeclaration = (): void => {
116
+ this.popFrame();
117
+ };
118
+
119
+ override enterVariableDeclaration = (
120
+ ctx: Parser.VariableDeclarationContext,
121
+ ): void => {
122
+ this.record(ctx.type(), ctx.IDENTIFIER());
123
+ };
124
+
125
+ override enterParameter = (ctx: Parser.ParameterContext): void => {
126
+ this.record(ctx.type(), ctx.IDENTIFIER());
127
+ };
128
+
129
+ override enterForVarDecl = (ctx: Parser.ForVarDeclContext): void => {
130
+ this.record(ctx.type(), ctx.IDENTIFIER());
131
+ };
132
+
133
+ // Each braced block (if/while/for body, and a function/scope body) is its own
134
+ // lexical scope, so a different-category redeclaration shadows only within the
135
+ // block instead of poisoning the name function-wide (Issue #1085 review).
136
+ override enterBlock = (ctx: Parser.BlockContext): void => {
137
+ this.pushFrame(ctx);
138
+ };
139
+
140
+ override exitBlock = (): void => {
141
+ this.popFrame();
142
+ };
143
+
144
+ // The for-loop header is its own scope so the loop variable is confined to the
145
+ // loop (header + body) and never overwrites an outer same-named variable.
146
+ override enterForStatement = (ctx: Parser.ForStatementContext): void => {
147
+ this.pushFrame(ctx);
148
+ };
149
+
150
+ override exitForStatement = (): void => {
151
+ this.popFrame();
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Second pass: detect binary operators combining mixed essential categories.
157
+ */
158
+ class MixedCategoryListener extends CNextListener {
159
+ private readonly analyzer: MixedTypeCategoryAnalyzer;
160
+
161
+ // eslint-disable-next-line @typescript-eslint/lines-between-class-members
162
+ private readonly globalFrame: ScopeFrame;
163
+
164
+ // eslint-disable-next-line @typescript-eslint/lines-between-class-members
165
+ private readonly frameOf: Map<ParserRuleContext, ScopeFrame>;
166
+
167
+ constructor(
168
+ analyzer: MixedTypeCategoryAnalyzer,
169
+ globalFrame: ScopeFrame,
170
+ frameOf: Map<ParserRuleContext, ScopeFrame>,
171
+ ) {
172
+ super();
173
+ this.analyzer = analyzer;
174
+ this.globalFrame = globalFrame;
175
+ this.frameOf = frameOf;
176
+ }
177
+
178
+ /** The scope frame enclosing an operand: nearest function/scope ancestor. */
179
+ private frameFor(ctx: ParserRuleContext): ScopeFrame {
180
+ let node: ParserRuleContext | null = ctx;
181
+ while (node) {
182
+ const frame = this.frameOf.get(node);
183
+ if (frame) return frame;
184
+ node = node.parent;
185
+ }
186
+ return this.globalFrame;
187
+ }
188
+
189
+ /** Map a known variable name to its essential type category within a scope. */
190
+ private categoryOfName(name: string, frame: ScopeFrame): Category {
191
+ let current: ScopeFrame | null = frame;
192
+ while (current) {
193
+ const typeName = current.vars.get(name);
194
+ if (typeName) {
195
+ if (TypeConstants.SIGNED_TYPES.includes(typeName)) return "signed";
196
+ if (TypeConstants.UNSIGNED_INT_TYPES.includes(typeName)) {
197
+ return "unsigned";
198
+ }
199
+ return null;
200
+ }
201
+ current = current.parent;
202
+ }
203
+ return null;
204
+ }
205
+
206
+ /**
207
+ * Collect the essential category of every classifiable VALUE leaf under one
208
+ * binary-operator operand, descending through nested operator levels and
209
+ * parentheses but never into a postfix suffix (an array index, bit-range
210
+ * argument, or call argument is not a value operand of THIS operator).
211
+ *
212
+ * A unary expression is a grammar leaf of the operator levels:
213
+ * - prefix `-`/`~` preserve the operand's category, so descend through them;
214
+ * - prefix `!` (essentially-Boolean result) and `&` (address-of, ADR-006)
215
+ * carry no signed/unsigned category — contribute null, so a mix like
216
+ * `!a = !b` is not falsely rejected (Issue #1085 review);
217
+ * - a postfix WITH a suffix (member/call/indexing/bit-extraction) cannot be
218
+ * positively classified — contribute null, which exempts the sanctioned
219
+ * cross-category form `x[0, 32]`;
220
+ * - a parenthesized expression contributes ALL of its own leaves (not merely
221
+ * the leftmost), so a compound operand is judged by its whole content.
222
+ */
223
+ private collectOperandCategories(
224
+ ctx: ParserRuleContext,
225
+ frame: ScopeFrame,
226
+ out: Category[],
227
+ ): void {
228
+ if (ctx instanceof Parser.UnaryExpressionContext) {
229
+ const inner = ctx.unaryExpression();
230
+ if (inner) {
231
+ const op = ctx.getChild(0)?.getText();
232
+ if (op === "!" || op === "&") {
233
+ out.push(null);
234
+ return;
235
+ }
236
+ this.collectOperandCategories(inner, frame, out);
237
+ return;
238
+ }
239
+
240
+ const postfix = ctx.postfixExpression();
241
+ if (!postfix || postfix.getChildCount() > 1) {
242
+ out.push(null);
243
+ return;
244
+ }
245
+
246
+ const primary = postfix.primaryExpression();
247
+ const parenthesized = primary?.expression();
248
+ if (parenthesized) {
249
+ this.collectOperandCategories(parenthesized, frame, out);
250
+ return;
251
+ }
252
+
253
+ const identifier = primary?.IDENTIFIER();
254
+ out.push(
255
+ identifier ? this.categoryOfName(identifier.getText(), frame) : null,
256
+ );
257
+ return;
258
+ }
259
+
260
+ for (let i = 0; i < ctx.getChildCount(); i += 1) {
261
+ const child = ctx.getChild(i);
262
+ if (child instanceof ParserRuleContext) {
263
+ this.collectOperandCategories(child, frame, out);
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * The essential category of one operand of a binary-operator level: the single
270
+ * category shared by all its classifiable value leaves, or null when it has
271
+ * none OR when its own leaves are themselves mixed.
272
+ *
273
+ * Returning null for an internally-mixed operand prevents a CASCADE of
274
+ * duplicate errors: `a * b + c` (with `i32 a`, `u32 b`, `u32 c`) is reported
275
+ * once — at the `a * b` level — instead of again at the `+ c` level, where the
276
+ * product's category is genuinely ambiguous rather than `a`'s leftmost
277
+ * (Issue #1085 review). An internally-mixed operand is always reported at its
278
+ * own level, so nothing is missed. Because a resolved (non-null) category
279
+ * means every classifiable leaf agrees, comparing two resolved-but-differing
280
+ * operands always reflects a real signed/unsigned combination — no false
281
+ * positive on uniform code.
282
+ */
283
+ private operandCategory(ctx: ParserRuleContext, frame: ScopeFrame): Category {
284
+ const leaves: Category[] = [];
285
+ this.collectOperandCategories(ctx, frame, leaves);
286
+
287
+ let resolved: Category = null;
288
+ for (const leaf of leaves) {
289
+ if (leaf === null) continue;
290
+ if (resolved === null) {
291
+ resolved = leaf;
292
+ } else if (resolved !== leaf) {
293
+ return null;
294
+ }
295
+ }
296
+ return resolved;
297
+ }
298
+
299
+ /**
300
+ * Compare adjacent operands at one binary-operator level and report any pair
301
+ * whose categories are both resolved and differ.
302
+ */
303
+ private checkLevel(operands: ParserRuleContext[]): void {
304
+ if (operands.length < 2) return;
305
+ const frame = this.frameFor(operands[0]);
306
+ for (let i = 0; i < operands.length - 1; i += 1) {
307
+ const left = this.operandCategory(operands[i], frame);
308
+ const right = this.operandCategory(operands[i + 1], frame);
309
+ if (left && right && left !== right) {
310
+ const { line, column } = ParserUtils.getPosition(operands[i + 1]);
311
+ this.analyzer.addError(line, column);
312
+ }
313
+ }
314
+ }
315
+
316
+ override enterMultiplicativeExpression = (
317
+ ctx: Parser.MultiplicativeExpressionContext,
318
+ ): void => {
319
+ this.checkLevel(ctx.unaryExpression());
320
+ };
321
+
322
+ override enterAdditiveExpression = (
323
+ ctx: Parser.AdditiveExpressionContext,
324
+ ): void => {
325
+ this.checkLevel(ctx.multiplicativeExpression());
326
+ };
327
+
328
+ override enterBitwiseAndExpression = (
329
+ ctx: Parser.BitwiseAndExpressionContext,
330
+ ): void => {
331
+ this.checkLevel(ctx.shiftExpression());
332
+ };
333
+
334
+ override enterBitwiseXorExpression = (
335
+ ctx: Parser.BitwiseXorExpressionContext,
336
+ ): void => {
337
+ this.checkLevel(ctx.bitwiseAndExpression());
338
+ };
339
+
340
+ override enterBitwiseOrExpression = (
341
+ ctx: Parser.BitwiseOrExpressionContext,
342
+ ): void => {
343
+ this.checkLevel(ctx.bitwiseXorExpression());
344
+ };
345
+
346
+ override enterRelationalExpression = (
347
+ ctx: Parser.RelationalExpressionContext,
348
+ ): void => {
349
+ this.checkLevel(ctx.bitwiseOrExpression());
350
+ };
351
+
352
+ override enterEqualityExpression = (
353
+ ctx: Parser.EqualityExpressionContext,
354
+ ): void => {
355
+ this.checkLevel(ctx.relationalExpression());
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Analyzer that detects binary operations mixing essential type categories.
361
+ */
362
+ class MixedTypeCategoryAnalyzer {
363
+ private errors: IMixedTypeCategoryError[] = [];
364
+
365
+ /**
366
+ * Analyze the parse tree for mixed-category binary operations.
367
+ */
368
+ public analyze(tree: Parser.ProgramContext): IMixedTypeCategoryError[] {
369
+ this.errors = [];
370
+
371
+ const collector = new ScopeCollector();
372
+ ParseTreeWalker.DEFAULT.walk(collector, tree);
373
+
374
+ const listener = new MixedCategoryListener(
375
+ this,
376
+ collector.getGlobalFrame(),
377
+ collector.getFrameOf(),
378
+ );
379
+ ParseTreeWalker.DEFAULT.walk(listener, tree);
380
+
381
+ return this.errors;
382
+ }
383
+
384
+ /**
385
+ * Add a mixed-category error.
386
+ */
387
+ public addError(line: number, column: number): void {
388
+ this.errors.push({
389
+ code: "E0810",
390
+ line,
391
+ column,
392
+ message:
393
+ "Binary operator combines operands of different essential type categories (signed and unsigned)",
394
+ helpText:
395
+ "MISRA C:2012 Rule 10.4: both operands must share an essential type category. " +
396
+ "Reinterpret one operand's bits to match the other with bit indexing, e.g. value[0, 32] (ADR-007/ADR-024).",
397
+ });
398
+ }
399
+
400
+ /**
401
+ * Get all detected errors.
402
+ */
403
+ public getErrors(): IMixedTypeCategoryError[] {
404
+ return this.errors;
405
+ }
406
+ }
407
+
408
+ export default MixedTypeCategoryAnalyzer;