c-next 0.1.48 → 0.1.50

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -189,7 +189,7 @@ interface GeneratorContext {
189
189
  indentLevel: number;
190
190
  scopeMembers: Map<string, Set<string>>; // scope -> member names (ADR-016)
191
191
  currentParameters: Map<string, TParameterInfo>; // ADR-006: track params for pointer semantics
192
- modifiedParameters: Set<string>; // Issue #268: track modified params for auto-const inference
192
+ // Issue #558: modifiedParameters removed - now uses analysis-phase results from this.modifiedParameters
193
193
  localArrays: Set<string>; // ADR-006: track local array variables (no & needed)
194
194
  localVariables: Set<string>; // ADR-016: track local variables (allowed as bare identifiers)
195
195
  floatBitShadows: Set<string>; // Track declared shadow variables for float bit indexing
@@ -221,7 +221,7 @@ export default class CodeGenerator implements IOrchestrator {
221
221
  indentLevel: 0,
222
222
  scopeMembers: new Map(), // ADR-016: renamed from namespaceMembers
223
223
  currentParameters: new Map(),
224
- modifiedParameters: new Set(),
224
+ // Issue #558: modifiedParameters removed - now uses analysis-phase results
225
225
  localArrays: new Set(),
226
226
  localVariables: new Set(), // ADR-016: track local variables
227
227
  floatBitShadows: new Set(), // Track declared shadow variables for float bit indexing
@@ -327,6 +327,17 @@ export default class CodeGenerator implements IOrchestrator {
327
327
  */
328
328
  private readonly modifiedParameters: Map<string, Set<string>> = new Map();
329
329
 
330
+ /**
331
+ * Issue #558: Pending cross-file modifications to inject after analyzePassByValue clears.
332
+ * Set by Pipeline before generate() to share modifications from previously processed files.
333
+ */
334
+ private pendingCrossFileModifications: Map<string, Set<string>> | null = null;
335
+
336
+ /**
337
+ * Issue #558: Pending cross-file parameter lists to inject for transitive propagation.
338
+ */
339
+ private pendingCrossFileParamLists: Map<string, string[]> | null = null;
340
+
330
341
  /**
331
342
  * Issue #269: Tracks which parameters should pass by value
332
343
  * Map of functionName -> Set of passByValue parameter names
@@ -1061,7 +1072,7 @@ export default class CodeGenerator implements IOrchestrator {
1061
1072
  this.context.localVariables.clear();
1062
1073
  this.context.floatBitShadows.clear();
1063
1074
  this.context.floatShadowCurrent.clear();
1064
- this.context.modifiedParameters.clear(); // Issue #268: Clear for new function
1075
+ // Issue #558: modifiedParameters tracking removed - uses analysis-phase results
1065
1076
  this.context.inFunctionBody = true;
1066
1077
  }
1067
1078
 
@@ -1106,13 +1117,14 @@ export default class CodeGenerator implements IOrchestrator {
1106
1117
 
1107
1118
  /**
1108
1119
  * Issue #268: Update symbol parameters with auto-const info.
1109
- * Call after generating function body to track unmodified parameters.
1120
+ * Issue #558: Now uses analysis-phase results for modification tracking.
1110
1121
  */
1111
1122
  updateFunctionParamsAutoConst(functionName: string): void {
1112
- // Collect unmodified parameters for this function
1123
+ // Collect unmodified parameters for this function using analysis results
1113
1124
  const unmodifiedParams = new Set<string>();
1125
+ const modifiedSet = this.modifiedParameters.get(functionName);
1114
1126
  for (const [paramName] of this.context.currentParameters) {
1115
- if (!this.context.modifiedParameters.has(paramName)) {
1127
+ if (!modifiedSet?.has(paramName)) {
1116
1128
  unmodifiedParams.add(paramName);
1117
1129
  }
1118
1130
  }
@@ -1121,11 +1133,100 @@ export default class CodeGenerator implements IOrchestrator {
1121
1133
 
1122
1134
  /**
1123
1135
  * Issue #268: Mark a parameter as modified for auto-const tracking.
1136
+ * Issue #558: Now a no-op - analysis phase handles all modification tracking
1137
+ * including transitive propagation across function calls and files.
1138
+ */
1139
+ markParameterModified(_paramName: string): void {
1140
+ // No-op: Analysis phase (analyzePassByValue) now handles all modification
1141
+ // tracking including cross-file and transitive propagation.
1142
+ }
1143
+
1144
+ /**
1145
+ * Issue #558: Check if a parameter is modified using analysis-phase results.
1146
+ * This is the unified source of truth for modification tracking.
1147
+ */
1148
+ private _isCurrentParameterModified(paramName: string): boolean {
1149
+ const funcName = this.context.currentFunctionName;
1150
+ if (!funcName) return false;
1151
+ return this.modifiedParameters.get(funcName)?.has(paramName) ?? false;
1152
+ }
1153
+
1154
+ /**
1155
+ * Issue #558: Get the modified parameters map for cross-file propagation.
1156
+ * Returns function name -> set of modified parameter names.
1157
+ */
1158
+ getModifiedParameters(): ReadonlyMap<string, Set<string>> {
1159
+ return this.modifiedParameters;
1160
+ }
1161
+
1162
+ /**
1163
+ * Issue #558: Set cross-file modification data to inject during analyzePassByValue.
1164
+ * Called by Pipeline before generate() to share modifications from previously processed files.
1165
+ */
1166
+ setCrossFileModifications(
1167
+ modifications: Map<string, Set<string>>,
1168
+ paramLists: Map<string, string[]>,
1169
+ ): void {
1170
+ this.pendingCrossFileModifications = modifications;
1171
+ this.pendingCrossFileParamLists = paramLists;
1172
+ }
1173
+
1174
+ /**
1175
+ * Issue #558: Get the function parameter lists for cross-file propagation.
1176
+ */
1177
+ getFunctionParamLists(): ReadonlyMap<string, string[]> {
1178
+ return this.functionParamLists;
1179
+ }
1180
+
1181
+ /**
1182
+ * Issue #561: Analyze modifications in a parse tree without full code generation.
1183
+ * Used by Pipeline.transpileSource() to collect modification info from includes
1184
+ * for cross-file const inference (unified with Pipeline.run() behavior).
1185
+ *
1186
+ * Returns the modifications and param lists discovered in this tree.
1124
1187
  */
1125
- markParameterModified(paramName: string): void {
1126
- if (this.context.currentParameters.has(paramName)) {
1127
- this.context.modifiedParameters.add(paramName);
1188
+ analyzeModificationsOnly(tree: Parser.ProgramContext): {
1189
+ modifications: Map<string, Set<string>>;
1190
+ paramLists: Map<string, string[]>;
1191
+ } {
1192
+ // Save current state
1193
+ const savedModifications = new Map(this.modifiedParameters);
1194
+ const savedParamLists = new Map(this.functionParamLists);
1195
+ const savedCallGraph = new Map(this.functionCallGraph);
1196
+
1197
+ // Clear for fresh analysis
1198
+ this.modifiedParameters.clear();
1199
+ this.functionParamLists.clear();
1200
+ this.functionCallGraph.clear();
1201
+
1202
+ // Run modification analysis on the tree
1203
+ this.collectFunctionParametersAndModifications(tree);
1204
+
1205
+ // Capture results before restoring state
1206
+ const modifications = new Map<string, Set<string>>();
1207
+ for (const [funcName, params] of this.modifiedParameters) {
1208
+ modifications.set(funcName, new Set(params));
1209
+ }
1210
+ const paramLists = new Map<string, string[]>();
1211
+ for (const [funcName, params] of this.functionParamLists) {
1212
+ paramLists.set(funcName, [...params]);
1213
+ }
1214
+
1215
+ // Restore previous state by clearing and repopulating (readonly maps)
1216
+ this.modifiedParameters.clear();
1217
+ for (const [k, v] of savedModifications) {
1218
+ this.modifiedParameters.set(k, v);
1128
1219
  }
1220
+ this.functionParamLists.clear();
1221
+ for (const [k, v] of savedParamLists) {
1222
+ this.functionParamLists.set(k, v);
1223
+ }
1224
+ this.functionCallGraph.clear();
1225
+ for (const [k, v] of savedCallGraph) {
1226
+ this.functionCallGraph.set(k, v);
1227
+ }
1228
+
1229
+ return { modifications, paramLists };
1129
1230
  }
1130
1231
 
1131
1232
  /**
@@ -1539,7 +1640,7 @@ export default class CodeGenerator implements IOrchestrator {
1539
1640
  indentLevel: 0,
1540
1641
  scopeMembers: new Map(), // ADR-016
1541
1642
  currentParameters: new Map(),
1542
- modifiedParameters: new Set(),
1643
+ // Issue #558: modifiedParameters removed - uses analysis-phase results
1543
1644
  localArrays: new Set(),
1544
1645
  localVariables: new Set(), // ADR-016
1545
1646
  floatBitShadows: new Set(), // Track declared shadow variables for float bit indexing
@@ -2025,6 +2126,29 @@ export default class CodeGenerator implements IOrchestrator {
2025
2126
  // Phase 1: Collect function parameter lists and direct modifications
2026
2127
  this.collectFunctionParametersAndModifications(tree);
2027
2128
 
2129
+ // Issue #558: Inject cross-file data before transitive propagation
2130
+ if (this.pendingCrossFileModifications) {
2131
+ for (const [funcName, params] of this.pendingCrossFileModifications) {
2132
+ const existing = this.modifiedParameters.get(funcName);
2133
+ if (existing) {
2134
+ for (const param of params) {
2135
+ existing.add(param);
2136
+ }
2137
+ } else {
2138
+ this.modifiedParameters.set(funcName, new Set(params));
2139
+ }
2140
+ }
2141
+ this.pendingCrossFileModifications = null; // Clear after use
2142
+ }
2143
+ if (this.pendingCrossFileParamLists) {
2144
+ for (const [funcName, params] of this.pendingCrossFileParamLists) {
2145
+ if (!this.functionParamLists.has(funcName)) {
2146
+ this.functionParamLists.set(funcName, params);
2147
+ }
2148
+ }
2149
+ this.pendingCrossFileParamLists = null; // Clear after use
2150
+ }
2151
+
2028
2152
  // Phase 2: Fixed-point iteration for transitive modifications
2029
2153
  this.propagateTransitiveModifications();
2030
2154
 
@@ -2120,17 +2244,31 @@ export default class CodeGenerator implements IOrchestrator {
2120
2244
  // Check for assignment statements
2121
2245
  if (stmt.assignmentStatement()) {
2122
2246
  const assign = stmt.assignmentStatement()!;
2123
- // Get the target - use assignmentTarget() which has IDENTIFIER()
2124
2247
  const target = assign.assignmentTarget();
2248
+
2249
+ // Issue #558: Extract base identifier from assignment target
2250
+ // - Simple identifier: x <- value
2251
+ // - Member access: x.field <- value (first IDENTIFIER is the base)
2252
+ // - Array access: x[i] <- value
2253
+ let baseIdentifier: string | null = null;
2254
+
2125
2255
  if (target?.IDENTIFIER()) {
2126
- // Simple identifier assignment (not array/member access)
2127
- if (!target.arrayAccess() && !target.memberAccess()) {
2128
- const targetName = target.IDENTIFIER()!.getText();
2129
- if (paramSet.has(targetName)) {
2130
- // Direct assignment to parameter
2131
- this.modifiedParameters.get(funcName)!.add(targetName);
2132
- }
2256
+ // Simple identifier assignment
2257
+ baseIdentifier = target.IDENTIFIER()!.getText();
2258
+ } else if (target?.memberAccess()) {
2259
+ // Member access: first IDENTIFIER is the base (e.g., cfg.value -> cfg)
2260
+ const identifiers = target.memberAccess()!.IDENTIFIER();
2261
+ if (identifiers.length > 0) {
2262
+ baseIdentifier = identifiers[0].getText();
2133
2263
  }
2264
+ } else if (target?.arrayAccess()) {
2265
+ // Array access: IDENTIFIER is the base (e.g., arr[0] -> arr)
2266
+ baseIdentifier = target.arrayAccess()!.IDENTIFIER()?.getText() ?? null;
2267
+ }
2268
+
2269
+ if (baseIdentifier && paramSet.has(baseIdentifier)) {
2270
+ // Direct or member/array assignment modifies the parameter
2271
+ this.modifiedParameters.get(funcName)!.add(baseIdentifier);
2134
2272
  }
2135
2273
  }
2136
2274
 
@@ -2386,9 +2524,17 @@ export default class CodeGenerator implements IOrchestrator {
2386
2524
  const memberNames: string[] = [];
2387
2525
 
2388
2526
  // Start with primary identifier if it's a scope name (not 'global' or 'this')
2527
+ // Issue #561: When 'this' is used, resolve to the current scope name from funcName
2389
2528
  const primaryId = primary.IDENTIFIER()?.getText();
2390
- if (primaryId && primaryId !== "global" && primaryId !== "this") {
2529
+ if (primaryId && primaryId !== "global") {
2391
2530
  memberNames.push(primaryId);
2531
+ } else if (primary.THIS()) {
2532
+ // Issue #561: 'this' keyword - resolve to current scope name from funcName
2533
+ // funcName format: "ScopeName_methodName" -> extract "ScopeName"
2534
+ const scopeName = funcName.split("_")[0];
2535
+ if (scopeName && scopeName !== funcName) {
2536
+ memberNames.push(scopeName);
2537
+ }
2392
2538
  }
2393
2539
 
2394
2540
  // Collect member access names until we hit a function call
@@ -5358,8 +5504,7 @@ export default class CodeGenerator implements IOrchestrator {
5358
5504
  // Track parameters for ADR-006 pointer semantics
5359
5505
  this._setParameters(ctx.parameterList() ?? null);
5360
5506
 
5361
- // Issue #268: Clear modified parameters tracking for this function
5362
- this.context.modifiedParameters.clear();
5507
+ // Issue #558: modifiedParameters tracking removed - uses analysis-phase results
5363
5508
 
5364
5509
  // ADR-016: Clear local variables and mark that we're in a function body
5365
5510
  this.context.localVariables.clear();
@@ -5525,8 +5670,8 @@ export default class CodeGenerator implements IOrchestrator {
5525
5670
  // Arrays pass naturally as pointers
5526
5671
  if (dims.length > 0) {
5527
5672
  const dimStr = dims.map((d) => this._generateArrayDimension(d)).join("");
5528
- // Issue #268: Add const for unmodified array parameters
5529
- const wasModified = this.context.modifiedParameters.has(name);
5673
+ // Issue #268/#558: Add const for unmodified array parameters (uses analysis results)
5674
+ const wasModified = this._isCurrentParameterModified(name);
5530
5675
  const autoConst = !wasModified && !constMod ? "const " : "";
5531
5676
  return `${autoConst}${constMod}${type} ${name}${dimStr}`;
5532
5677
  }
@@ -5557,8 +5702,8 @@ export default class CodeGenerator implements IOrchestrator {
5557
5702
  // ADR-045: String parameters (non-array) are passed as char*
5558
5703
  // Issue #551: Handle before unknown type check
5559
5704
  if (ctx.type().stringType() && dims.length === 0) {
5560
- // Issue #268: Add const for unmodified string parameters
5561
- const wasModified = this.context.modifiedParameters.has(name);
5705
+ // Issue #268/#558: Add const for unmodified string parameters (uses analysis results)
5706
+ const wasModified = this._isCurrentParameterModified(name);
5562
5707
  const autoConst = !wasModified && !constMod ? "const " : "";
5563
5708
  return `${autoConst}${constMod}char* ${name}`;
5564
5709
  }
@@ -5566,8 +5711,8 @@ export default class CodeGenerator implements IOrchestrator {
5566
5711
  // ADR-006: Pass by reference for known struct types and known primitives
5567
5712
  // Issue #551: Unknown types (external enums, typedefs) use pass-by-value
5568
5713
  if (this._isKnownStruct(typeName) || this._isKnownPrimitive(typeName)) {
5569
- // Issue #268: Add const for unmodified pointer parameters
5570
- const wasModified = this.context.modifiedParameters.has(name);
5714
+ // Issue #268/#558: Add const for unmodified pointer parameters (uses analysis results)
5715
+ const wasModified = this._isCurrentParameterModified(name);
5571
5716
  const autoConst = !wasModified && !constMod ? "const " : "";
5572
5717
  // Issue #409: In C++ mode, use references (&) instead of pointers (*)
5573
5718
  // This allows C-Next callbacks to match C++ function pointer signatures
@@ -6833,10 +6978,7 @@ export default class CodeGenerator implements IOrchestrator {
6833
6978
  throw new Error(constError);
6834
6979
  }
6835
6980
 
6836
- // Issue #268: Track parameter modification for auto-const inference
6837
- if (this.context.currentParameters.has(id)) {
6838
- this.context.modifiedParameters.add(id);
6839
- }
6981
+ // Issue #558: Parameter modification tracking removed - uses analysis-phase results
6840
6982
 
6841
6983
  // Invalidate float shadow when variable is assigned directly
6842
6984
  const shadowName = `__bits_${id}`;
@@ -6962,10 +7104,7 @@ export default class CodeGenerator implements IOrchestrator {
6962
7104
  );
6963
7105
  }
6964
7106
 
6965
- // Issue #268: Track parameter modification for auto-const inference (array element)
6966
- if (this.context.currentParameters.has(arrayName)) {
6967
- this.context.modifiedParameters.add(arrayName);
6968
- }
7107
+ // Issue #558: Parameter modification tracking removed - uses analysis-phase results
6969
7108
  }
6970
7109
 
6971
7110
  // Check member access on const struct - validate the root is not const
@@ -6978,10 +7117,7 @@ export default class CodeGenerator implements IOrchestrator {
6978
7117
  throw new Error(`${constError} (member access)`);
6979
7118
  }
6980
7119
 
6981
- // Issue #268: Track parameter modification for auto-const inference (member access)
6982
- if (this.context.currentParameters.has(rootName)) {
6983
- this.context.modifiedParameters.add(rootName);
6984
- }
7120
+ // Issue #558: Parameter modification tracking removed - uses analysis-phase results
6985
7121
 
6986
7122
  // ADR-013: Check for read-only register members (ro = implicitly const)
6987
7123
  if (identifiers.length >= 2) {
@@ -7124,7 +7260,8 @@ export default class CodeGenerator implements IOrchestrator {
7124
7260
  !paramInfo.isStruct &&
7125
7261
  this._isKnownPrimitive(paramInfo.baseType)
7126
7262
  ) {
7127
- return `(*${id})`;
7263
+ // Issue #558: In C++ mode, primitives that become references don't need dereferencing
7264
+ return this.cppMode ? id : `(*${id})`;
7128
7265
  }
7129
7266
  return id;
7130
7267
  }
@@ -8657,7 +8794,8 @@ export default class CodeGenerator implements IOrchestrator {
8657
8794
  !paramInfo.isStruct &&
8658
8795
  this._isKnownPrimitive(paramInfo.baseType)
8659
8796
  ) {
8660
- return `(*${id})`;
8797
+ // Issue #558: In C++ mode, primitives that become references don't need dereferencing
8798
+ return this.cppMode ? id : `(*${id})`;
8661
8799
  }
8662
8800
  return id;
8663
8801
  }
@@ -82,6 +82,17 @@ class Pipeline {
82
82
  * Used to include C headers in generated .h files instead of forward-declaring types.
83
83
  */
84
84
  private readonly headerIncludeDirectives: Map<string, string> = new Map();
85
+ /**
86
+ * Issue #558: Accumulated parameter modifications across all processed files.
87
+ * Used for cross-file const inference in C++ mode.
88
+ */
89
+ private readonly accumulatedModifications: Map<string, Set<string>> =
90
+ new Map();
91
+ /**
92
+ * Issue #558: Accumulated function parameter lists across all processed files.
93
+ * Used for cross-file const inference transitive propagation.
94
+ */
95
+ private readonly accumulatedParamLists: Map<string, string[]> = new Map();
85
96
 
86
97
  constructor(config: IPipelineConfig) {
87
98
  // Apply defaults
@@ -137,6 +148,10 @@ class Pipeline {
137
148
  await this.cacheManager.initialize();
138
149
  }
139
150
 
151
+ // Issue #558: Reset cross-file modification tracking for new run
152
+ this.accumulatedModifications.clear();
153
+ this.accumulatedParamLists.clear();
154
+
140
155
  // Stage 1: Discover source files
141
156
  const { cnextFiles, headerFiles } = await this.discoverSources();
142
157
 
@@ -146,6 +161,15 @@ class Pipeline {
146
161
  return result;
147
162
  }
148
163
 
164
+ // Issue #558: Sort files by dependency order for correct cross-file const inference.
165
+ // Files that are included by others should be processed first so their
166
+ // parameter modifications are available during transitive propagation.
167
+ // Simple heuristic: reverse the discovery order since includes are added
168
+ // after the files that include them.
169
+ if (this.cppDetected) {
170
+ cnextFiles.reverse();
171
+ }
172
+
149
173
  // Ensure output directory exists if specified
150
174
  if (this.config.outDir && !existsSync(this.config.outDir)) {
151
175
  mkdirSync(this.config.outDir, { recursive: true });
@@ -575,6 +599,7 @@ class Pipeline {
575
599
 
576
600
  /**
577
601
  * Stage 3: Collect symbols from a C-Next file
602
+ * Issue #561: Also collects modification analysis in C++ mode for unified cross-file const inference
578
603
  */
579
604
  private collectCNextSymbols(file: IDiscoveredFile): void {
580
605
  const content = readFileSync(file.path, "utf-8");
@@ -616,6 +641,32 @@ class Pipeline {
616
641
  // This ensures enum member info is available for all files before code generation
617
642
  const symbolInfo = TSymbolInfoAdapter.convert(tSymbols);
618
643
  this.symbolInfoByFile.set(file.path, symbolInfo);
644
+
645
+ // Issue #561: Collect modification analysis in C++ mode for cross-file const inference
646
+ // This unifies behavior between run() and transpileSource() code paths
647
+ if (this.cppDetected) {
648
+ const { modifications, paramLists } =
649
+ this.codeGenerator.analyzeModificationsOnly(tree);
650
+
651
+ // Accumulate modifications
652
+ for (const [funcName, params] of modifications) {
653
+ const existing = this.accumulatedModifications.get(funcName);
654
+ if (existing) {
655
+ for (const param of params) {
656
+ existing.add(param);
657
+ }
658
+ } else {
659
+ this.accumulatedModifications.set(funcName, new Set(params));
660
+ }
661
+ }
662
+
663
+ // Accumulate param lists
664
+ for (const [funcName, params] of paramLists) {
665
+ if (!this.accumulatedParamLists.has(funcName)) {
666
+ this.accumulatedParamLists.set(funcName, [...params]);
667
+ }
668
+ }
669
+ }
619
670
  }
620
671
 
621
672
  /**
@@ -788,6 +839,14 @@ class Pipeline {
788
839
  );
789
840
  }
790
841
 
842
+ // Issue #558: Inject cross-file data for const inference
843
+ if (this.cppDetected && this.accumulatedModifications.size > 0) {
844
+ this.codeGenerator.setCrossFileModifications(
845
+ this.accumulatedModifications,
846
+ this.accumulatedParamLists,
847
+ );
848
+ }
849
+
791
850
  const code = this.codeGenerator.generate(
792
851
  tree,
793
852
  this.symbolTable,
@@ -822,6 +881,27 @@ class Pipeline {
822
881
  }
823
882
  this.passByValueParamsCollectors.set(file.path, passByValueCopy);
824
883
 
884
+ // Issue #558: Collect modifications and param lists for cross-file const inference
885
+ if (this.cppDetected) {
886
+ const fileModifications = this.codeGenerator.getModifiedParameters();
887
+ for (const [funcName, params] of fileModifications) {
888
+ const existing = this.accumulatedModifications.get(funcName);
889
+ if (existing) {
890
+ for (const param of params) {
891
+ existing.add(param);
892
+ }
893
+ } else {
894
+ this.accumulatedModifications.set(funcName, new Set(params));
895
+ }
896
+ }
897
+ const fileParamLists = this.codeGenerator.getFunctionParamLists();
898
+ for (const [funcName, params] of fileParamLists) {
899
+ if (!this.accumulatedParamLists.has(funcName)) {
900
+ this.accumulatedParamLists.set(funcName, [...params]);
901
+ }
902
+ }
903
+ }
904
+
825
905
  // Issue #424: Store user includes for header generation
826
906
  // These may define macros used in array dimensions
827
907
  // Issue #461: Transform .cnx includes to .h for header generation
@@ -1188,6 +1268,11 @@ class Pipeline {
1188
1268
  await this.cacheManager.initialize();
1189
1269
  }
1190
1270
 
1271
+ // Issue #561: Clear cross-file modification tracking for fresh analysis
1272
+ // This ensures each transpileSource() call starts with a clean slate
1273
+ this.accumulatedModifications.clear();
1274
+ this.accumulatedParamLists.clear();
1275
+
1191
1276
  // Step 1: Build search paths using unified IncludeResolver
1192
1277
  const searchPaths = IncludeResolver.buildSearchPaths(
1193
1278
  workingDir,
@@ -1362,6 +1447,15 @@ class Pipeline {
1362
1447
  );
1363
1448
  }
1364
1449
 
1450
+ // Issue #561: Inject cross-file modification data for const inference
1451
+ // This unifies behavior with run() - both paths now share modifications from includes
1452
+ if (this.cppDetected && this.accumulatedModifications.size > 0) {
1453
+ this.codeGenerator.setCrossFileModifications(
1454
+ this.accumulatedModifications,
1455
+ this.accumulatedParamLists,
1456
+ );
1457
+ }
1458
+
1365
1459
  const code = this.codeGenerator.generate(
1366
1460
  tree,
1367
1461
  this.symbolTable,
@@ -63,6 +63,13 @@ class Project {
63
63
  * Compile the project
64
64
  */
65
65
  async compile(): Promise<IProjectResult> {
66
+ // Issue #558: For C++ mode, use Pipeline.run() to enable cross-file const inference.
67
+ // This is needed because struct parameters passed to modifying functions in other
68
+ // files must not be marked const.
69
+ if (this.config.cppRequired) {
70
+ return this.compileWithPipeline();
71
+ }
72
+
66
73
  // Compile each input file individually to avoid cross‑file symbol leakage.
67
74
  const results: any[] = [];
68
75
  // Build the full list of .cnx/.cnext files from srcDirs and explicit files
@@ -175,6 +182,30 @@ class Project {
175
182
 
176
183
  return aggregate;
177
184
  }
185
+
186
+ /**
187
+ * Issue #558: Compile using Pipeline.run() for cross-file const inference in C++ mode.
188
+ */
189
+ private async compileWithPipeline(): Promise<IProjectResult> {
190
+ const pipelineResult = await this.pipeline.run();
191
+
192
+ // Convert Pipeline result to Project result format
193
+ return {
194
+ success: pipelineResult.success,
195
+ filesProcessed: pipelineResult.filesProcessed,
196
+ symbolsCollected: pipelineResult.symbolsCollected,
197
+ conflicts: pipelineResult.conflicts,
198
+ errors: pipelineResult.errors.map((e) => {
199
+ if (typeof e === "string") return e;
200
+ const path = (e as any).sourcePath ?? "";
201
+ return path
202
+ ? `${path}:${e.line}:${e.column} ${e.message}`
203
+ : `${e.line}:${e.column} ${e.message}`;
204
+ }),
205
+ warnings: pipelineResult.warnings,
206
+ outputFiles: pipelineResult.outputFiles,
207
+ };
208
+ }
178
209
  /**
179
210
  * Get the symbol table (for testing/inspection)
180
211
  */