c-next 0.2.17 → 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 (81) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +7645 -5941
  3. package/dist/index.js.map +4 -4
  4. package/grammar/CNext.g4 +8 -0
  5. package/package.json +1 -3
  6. package/src/transpiler/Transpiler.ts +286 -26
  7. package/src/transpiler/__tests__/compileCommandsDiscovery.integration.test.ts +94 -0
  8. package/src/transpiler/__tests__/externalSymbolRecovery.integration.test.ts +215 -0
  9. package/src/transpiler/logic/__tests__/detectAssemblySyntax.test.ts +116 -0
  10. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +8 -0
  11. package/src/transpiler/logic/analysis/MixedTypeCategoryAnalyzer.ts +408 -0
  12. package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +16 -97
  13. package/src/transpiler/logic/analysis/ReturnPathAnalyzer.ts +171 -0
  14. package/src/transpiler/logic/analysis/__tests__/MixedTypeCategoryAnalyzer.test.ts +346 -0
  15. package/src/transpiler/logic/analysis/__tests__/ReturnPathAnalyzer.test.ts +232 -0
  16. package/src/transpiler/logic/analysis/runAnalyzers.ts +23 -2
  17. package/src/transpiler/logic/analysis/types/IMixedTypeCategoryError.ts +17 -0
  18. package/src/transpiler/logic/analysis/types/IReturnPathError.ts +12 -0
  19. package/src/transpiler/logic/detectAssemblySyntax.ts +80 -0
  20. package/src/transpiler/logic/parser/grammar/CNext.interp +4 -1
  21. package/src/transpiler/logic/parser/grammar/CNext.tokens +169 -167
  22. package/src/transpiler/logic/parser/grammar/CNextLexer.interp +4 -1
  23. package/src/transpiler/logic/parser/grammar/CNextLexer.tokens +169 -167
  24. package/src/transpiler/logic/parser/grammar/CNextLexer.ts +545 -541
  25. package/src/transpiler/logic/parser/grammar/CNextListener.ts +11 -0
  26. package/src/transpiler/logic/parser/grammar/CNextParser.ts +1257 -1185
  27. package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +7 -0
  28. package/src/transpiler/logic/preprocessor/CompileCommandsReader.ts +332 -0
  29. package/src/transpiler/logic/preprocessor/ExternalDeclarationOracle.ts +202 -0
  30. package/src/transpiler/logic/preprocessor/Preprocessor.ts +24 -6
  31. package/src/transpiler/logic/preprocessor/ToolchainDetector.ts +42 -1
  32. package/src/transpiler/logic/preprocessor/__tests__/CompileCommandsReader.test.ts +216 -0
  33. package/src/transpiler/logic/preprocessor/__tests__/ExternalDeclarationOracle.test.ts +153 -0
  34. package/src/transpiler/logic/preprocessor/__tests__/Preprocessor.test.ts +43 -18
  35. package/src/transpiler/logic/preprocessor/__tests__/ToolchainDetector.crossCompiler.test.ts +75 -0
  36. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/dependent.h +7 -0
  37. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/predecessor.h +5 -0
  38. package/src/transpiler/logic/preprocessor/types/ICompileCommandsResult.ts +14 -0
  39. package/src/transpiler/logic/preprocessor/types/IPreprocessOptions.ts +17 -0
  40. package/src/transpiler/logic/symbols/SymbolTable.ts +45 -0
  41. package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +49 -0
  42. package/src/transpiler/output/MisraSuppressionUtils.ts +52 -0
  43. package/src/transpiler/output/__tests__/MisraSuppressionUtils.test.ts +67 -0
  44. package/src/transpiler/output/codegen/CodeGenerator.ts +45 -4
  45. package/src/transpiler/output/codegen/TypeResolver.ts +148 -0
  46. package/src/transpiler/output/codegen/TypeValidator.ts +179 -81
  47. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +13 -1
  48. package/src/transpiler/output/codegen/__tests__/TypeResolver.test.ts +93 -0
  49. package/src/transpiler/output/codegen/__tests__/TypeValidator.alwaysTrueLoop.test.ts +91 -0
  50. package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +86 -14
  51. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +13 -0
  52. package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +1 -1
  53. package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +20 -12
  54. package/src/transpiler/output/codegen/assignment/handlers/ArrayHandlers.ts +424 -22
  55. package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +31 -18
  56. package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +7 -8
  57. package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +6 -8
  58. package/src/transpiler/output/codegen/assignment/handlers/RegisterUtils.ts +12 -10
  59. package/src/transpiler/output/codegen/assignment/handlers/SimpleHandler.ts +3 -7
  60. package/src/transpiler/output/codegen/assignment/handlers/SpecialHandlers.ts +7 -9
  61. package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +12 -10
  62. package/src/transpiler/output/codegen/assignment/handlers/__tests__/ArrayHandlers.test.ts +479 -11
  63. package/src/transpiler/output/codegen/generators/IOrchestrator.ts +3 -0
  64. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +28 -15
  65. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +26 -13
  66. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +129 -1
  67. package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +64 -8
  68. package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +8 -5
  69. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +30 -2
  70. package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +16 -13
  71. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +19 -0
  72. package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +57 -11
  73. package/src/transpiler/output/codegen/subscript/SubscriptClassifier.ts +30 -11
  74. package/src/transpiler/output/codegen/subscript/TSubscriptKind.ts +1 -1
  75. package/src/transpiler/output/codegen/subscript/__tests__/SubscriptClassifier.test.ts +38 -6
  76. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +65 -24
  77. package/src/transpiler/state/CodeGenState.ts +20 -16
  78. package/src/transpiler/types/ICachedFileEntry.ts +7 -0
  79. package/src/utils/cache/CacheManager.ts +13 -2
  80. package/src/utils/cache/__tests__/CacheManager.test.ts +18 -1
  81. package/src/utils/constants/TypeConstants.ts +13 -0
@@ -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;
@@ -14,7 +14,12 @@
14
14
  * 1. It's a small primitive type (u8, i8, u16, i16, u32, i32, u64, i64, bool)
15
15
  * 2. It's not modified (directly or transitively)
16
16
  * 3. It's not an array, struct, string, or callback
17
- * 4. It's not accessed via subscript (Issue #579)
17
+ *
18
+ * Issue #1100: Subscript access no longer forces pointer semantics on its
19
+ * own. A scalar parameter subscripted with a single index is bit-indexing
20
+ * (ADR-007), not array access, so it stays eligible for pass-by-value.
21
+ * Only genuine array parameters (`isArray`, from explicit `T[N]` syntax,
22
+ * ADR-006) are excluded — via the isArray check below.
18
23
  */
19
24
 
20
25
  import * as Parser from "../parser/grammar/CNextParser";
@@ -173,8 +178,6 @@ class PassByValueAnalyzer {
173
178
 
174
179
  // Initialize modified set
175
180
  CodeGenState.modifiedParameters.set(funcName, new Set());
176
- // Issue #579: Initialize subscript access tracking
177
- CodeGenState.subscriptAccessedParameters.set(funcName, new Set());
178
181
  CodeGenState.functionCallGraph.set(funcName, []);
179
182
 
180
183
  // Walk the function body to find modifications and calls
@@ -225,15 +228,9 @@ class PassByValueAnalyzer {
225
228
  );
226
229
  }
227
230
 
228
- // 2. Walk all expressions in this statement for function calls and subscript access
231
+ // 2. Walk all expressions in this statement for function calls
229
232
  for (const expr of StatementExpressionCollector.collectAll(stmt)) {
230
233
  PassByValueAnalyzer.walkExpressionForCalls(funcName, paramSet, expr);
231
- // Issue #579: Also track subscript read access on parameters
232
- PassByValueAnalyzer.walkExpressionForSubscriptAccess(
233
- funcName,
234
- paramSet,
235
- expr,
236
- );
237
234
  }
238
235
 
239
236
  // 3. Recurse into child statements and blocks
@@ -266,21 +263,11 @@ class PassByValueAnalyzer {
266
263
  const assign = stmt.assignmentStatement()!;
267
264
  const target = assign.assignmentTarget();
268
265
 
269
- const { baseIdentifier, hasSingleIndexSubscript } =
270
- AssignmentTargetExtractor.extract(target);
271
-
272
- // Issue #579: Track subscript access on parameters (for write path)
273
- if (
274
- hasSingleIndexSubscript &&
275
- baseIdentifier &&
276
- paramSet.has(baseIdentifier)
277
- ) {
278
- CodeGenState.subscriptAccessedParameters
279
- .get(funcName)!
280
- .add(baseIdentifier);
281
- }
266
+ const { baseIdentifier } = AssignmentTargetExtractor.extract(target);
282
267
 
283
- // Track as modified parameter
268
+ // Track as modified parameter (covers both `x <- value` and subscripted
269
+ // writes like `x[i] <- value` / `x[4] <- true` — both change x's value,
270
+ // so x must pass by pointer for the caller to observe the change)
284
271
  if (baseIdentifier && paramSet.has(baseIdentifier)) {
285
272
  CodeGenState.modifiedParameters.get(funcName)!.add(baseIdentifier);
286
273
  }
@@ -309,67 +296,9 @@ class PassByValueAnalyzer {
309
296
  }
310
297
  }
311
298
 
312
- /**
313
- * Issue #579: Walk an expression tree to find subscript access on parameters.
314
- * This tracks read access like `buf[i]` where buf is a parameter.
315
- * Parameters with subscript access must become pointers.
316
- */
317
- private static walkExpressionForSubscriptAccess(
318
- funcName: string,
319
- paramSet: Set<string>,
320
- expr: Parser.ExpressionContext,
321
- ): void {
322
- const ternary = expr.ternaryExpression();
323
- if (ternary) {
324
- for (const orExpr of ternary.orExpression()) {
325
- PassByValueAnalyzer.walkOrExpression(orExpr, (unaryExpr) => {
326
- PassByValueAnalyzer.handleSubscriptAccess(
327
- funcName,
328
- paramSet,
329
- unaryExpr,
330
- );
331
- });
332
- }
333
- }
334
- }
335
-
336
- /**
337
- * Issue #579: Handle subscript access on a unary expression.
338
- * Only tracks single-index subscript access (which could be array access).
339
- * Two-index subscript (e.g., value[start, width]) is always bit extraction,
340
- * so it doesn't require the parameter to become a pointer.
341
- */
342
- private static handleSubscriptAccess(
343
- funcName: string,
344
- paramSet: Set<string>,
345
- unaryExpr: Parser.UnaryExpressionContext,
346
- ): void {
347
- const postfixExpr = unaryExpr.postfixExpression();
348
- if (!postfixExpr) return;
349
-
350
- const primary = postfixExpr.primaryExpression();
351
- const ops = postfixExpr.postfixOp();
352
-
353
- // Check if primary is a parameter and there's subscript access
354
- const primaryId = primary.IDENTIFIER()?.getText();
355
- if (!primaryId || !paramSet.has(primaryId)) {
356
- return;
357
- }
358
-
359
- // Only track SINGLE-index subscript access (potential array access)
360
- // Two-index subscript like value[0, 8] is bit extraction, not array access
361
- const hasSingleIndexSubscript = ops.some(
362
- (op) => op.expression().length === 1,
363
- );
364
- if (hasSingleIndexSubscript) {
365
- CodeGenState.subscriptAccessedParameters.get(funcName)!.add(primaryId);
366
- }
367
- }
368
-
369
299
  /**
370
300
  * Generic walker for orExpression trees.
371
301
  * Walks through the expression hierarchy and calls the handler for each unaryExpression.
372
- * Used by both function call tracking and subscript access tracking.
373
302
  */
374
303
  private static walkOrExpression(
375
304
  orExpr: Parser.OrExpressionContext,
@@ -671,24 +600,14 @@ class PassByValueAnalyzer {
671
600
 
672
601
  // Check if eligible for pass-by-value:
673
602
  // - Is a small primitive type
674
- // - Not an array
675
- // - Not modified
676
- // - Not accessed via subscript (Issue #579)
603
+ // - Not an array (array parameters always decay to pointers, ADR-006)
604
+ // - Not modified (a subscripted bit-write, e.g. `x[4] <- true`, counts
605
+ // as a modification and is already tracked in `modified` above)
677
606
  const isSmallPrimitive = SMALL_PRIMITIVES.has(paramSig.baseType);
678
607
  const isArray = paramSig.isArray ?? false;
679
608
  const isModified = modified.has(paramName);
680
- // Issue #579: Parameters with subscript access must become pointers
681
- const hasSubscriptAccess =
682
- CodeGenState.subscriptAccessedParameters
683
- .get(funcName)
684
- ?.has(paramName) ?? false;
685
-
686
- if (
687
- isSmallPrimitive &&
688
- !isArray &&
689
- !isModified &&
690
- !hasSubscriptAccess
691
- ) {
609
+
610
+ if (isSmallPrimitive && !isArray && !isModified) {
692
611
  passByValue.add(paramName);
693
612
  }
694
613
  }