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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
5
  "packageManager": "npm@11.9.0",
6
6
  "type": "module",
package/src/cli/Runner.ts CHANGED
@@ -54,7 +54,7 @@ class Runner {
54
54
  debugMode: config.debugMode,
55
55
  });
56
56
 
57
- const result = await pipeline.run();
57
+ const result = await pipeline.transpile({ kind: "files" });
58
58
  this._renameOutputIfNeeded(result, explicitOutputFile);
59
59
 
60
60
  ResultPrinter.print(result);
@@ -30,7 +30,7 @@ describe("Runner", () => {
30
30
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
31
  let processExitSpy: any;
32
32
  let mockConfig: ICliConfig;
33
- let mockTranspilerInstance: { run: ReturnType<typeof vi.fn> };
33
+ let mockTranspilerInstance: { transpile: ReturnType<typeof vi.fn> };
34
34
 
35
35
  beforeEach(() => {
36
36
  vi.clearAllMocks();
@@ -66,7 +66,7 @@ describe("Runner", () => {
66
66
 
67
67
  // Mock Transpiler
68
68
  mockTranspilerInstance = {
69
- run: vi.fn().mockResolvedValue({
69
+ transpile: vi.fn().mockResolvedValue({
70
70
  success: true,
71
71
  outputFiles: ["/project/src/main.c"],
72
72
  errors: [],
@@ -94,7 +94,7 @@ describe("Runner", () => {
94
94
  "src/main.cnx",
95
95
  ]);
96
96
  expect(Transpiler).toHaveBeenCalled();
97
- expect(mockTranspilerInstance.run).toHaveBeenCalled();
97
+ expect(mockTranspilerInstance.transpile).toHaveBeenCalled();
98
98
  expect(ResultPrinter.print).toHaveBeenCalled();
99
99
  });
100
100
 
@@ -125,7 +125,7 @@ describe("Runner", () => {
125
125
  });
126
126
 
127
127
  it("passes include paths to Transpiler", async () => {
128
- // Include discovery now happens inside Transpiler.discoverSources()
128
+ // Include discovery now happens inside Transpiler.discoverIncludes()
129
129
  // Runner just passes config.includeDirs directly
130
130
  mockConfig.includeDirs = ["/extra/include"];
131
131
 
@@ -168,7 +168,7 @@ describe("Runner", () => {
168
168
  it("handles explicit output filename for single file", async () => {
169
169
  mockConfig.outputPath = "output/result.c";
170
170
 
171
- mockTranspilerInstance.run.mockResolvedValue({
171
+ mockTranspilerInstance.transpile.mockResolvedValue({
172
172
  success: true,
173
173
  outputFiles: ["/project/output/main.c"],
174
174
  errors: [],
@@ -236,7 +236,7 @@ describe("Runner", () => {
236
236
  });
237
237
 
238
238
  it("exits with 0 on success", async () => {
239
- mockTranspilerInstance.run.mockResolvedValue({
239
+ mockTranspilerInstance.transpile.mockResolvedValue({
240
240
  success: true,
241
241
  outputFiles: ["/project/src/main.c"],
242
242
  errors: [],
@@ -248,7 +248,7 @@ describe("Runner", () => {
248
248
  });
249
249
 
250
250
  it("exits with 1 on failure", async () => {
251
- mockTranspilerInstance.run.mockResolvedValue({
251
+ mockTranspilerInstance.transpile.mockResolvedValue({
252
252
  success: false,
253
253
  outputFiles: [],
254
254
  errors: ["Some error"],
@@ -285,7 +285,7 @@ describe("Runner", () => {
285
285
  it("doesn't rename when generated file matches explicit path", async () => {
286
286
  mockConfig.outputPath = "/project/output/main.c";
287
287
 
288
- mockTranspilerInstance.run.mockResolvedValue({
288
+ mockTranspilerInstance.transpile.mockResolvedValue({
289
289
  success: true,
290
290
  outputFiles: ["/project/output/main.c"],
291
291
  errors: [],
@@ -19,7 +19,7 @@ import parseWithSymbols from "../../lib/parseWithSymbols";
19
19
  import parseCHeader from "../../lib/parseCHeader";
20
20
 
21
21
  /**
22
- * Method handler type (async to support Transpiler.transpileSource)
22
+ * Method handler type (async to support Transpiler.transpile)
23
23
  */
24
24
  type MethodHandler = (
25
25
  params?: Record<string, unknown>,
@@ -286,17 +286,35 @@ class ServeCommand {
286
286
  ? { workingDir: dirname(filePath), sourcePath: filePath }
287
287
  : undefined;
288
288
 
289
- const result = await ServeCommand.transpiler.transpileSource(
289
+ const transpileResult = await ServeCommand.transpiler.transpile({
290
+ kind: "source",
290
291
  source,
291
- options,
292
- );
292
+ ...options,
293
+ });
294
+ const fileResult =
295
+ transpileResult.files.find(
296
+ (f) => f.sourcePath === (filePath ?? "<string>"),
297
+ ) ?? transpileResult.files[0];
298
+
299
+ // When files[] is empty (e.g., parse errors), fall back to aggregate errors
300
+ if (!fileResult) {
301
+ return {
302
+ success: true,
303
+ result: {
304
+ success: false,
305
+ code: "",
306
+ errors: transpileResult.errors,
307
+ cppDetected: ServeCommand.transpiler.isCppDetected(),
308
+ },
309
+ };
310
+ }
293
311
 
294
312
  return {
295
313
  success: true,
296
314
  result: {
297
- success: result.success,
298
- code: result.code,
299
- errors: result.errors,
315
+ success: fileResult.success,
316
+ code: fileResult.code,
317
+ errors: fileResult.errors,
300
318
  cppDetected: ServeCommand.transpiler.isCppDetected(),
301
319
  },
302
320
  };
@@ -312,12 +330,14 @@ class ServeCommand {
312
330
  ): Promise<IMethodResult> {
313
331
  const { source, filePath } = params;
314
332
 
315
- // If transpiler is initialized, run transpileSource to trigger header
333
+ // If transpiler is initialized, run transpile to trigger header
316
334
  // resolution and C++ detection (results are discarded, we just want
317
335
  // the side effects on the symbol table)
318
336
  if (ServeCommand.transpiler && filePath) {
319
337
  try {
320
- await ServeCommand.transpiler.transpileSource(source, {
338
+ await ServeCommand.transpiler.transpile({
339
+ kind: "source",
340
+ source,
321
341
  workingDir: dirname(filePath),
322
342
  sourcePath: filePath,
323
343
  });
@@ -5,9 +5,9 @@
5
5
  * Key insight from ADR-053: "A single file transpilation is just a project
6
6
  * with one .cnx file."
7
7
  *
8
- * Architecture: Both run() and transpileSource() are thin wrappers that
9
- * discover files and delegate to _executePipeline(). There is ONE pipeline
10
- * for all transpilation — no branching on context/standalone mode.
8
+ * Architecture: transpile() is the single entry point. It discovers files
9
+ * via discoverIncludes(), then delegates to _executePipeline(). There is
10
+ * ONE pipeline for all transpilation.
11
11
  */
12
12
 
13
13
  import { join, basename, dirname, resolve } from "node:path";
@@ -52,6 +52,7 @@ import ITranspilerResult from "./types/ITranspilerResult";
52
52
  import IFileResult from "./types/IFileResult";
53
53
  import IPipelineFile from "./types/IPipelineFile";
54
54
  import IPipelineInput from "./types/IPipelineInput";
55
+ import TTranspileInput from "./types/TTranspileInput";
55
56
  import ITranspileError from "../lib/types/ITranspileError";
56
57
  import TranspilerState from "./state/TranspilerState";
57
58
  import runAnalyzers from "./logic/analysis/runAnalyzers";
@@ -134,42 +135,33 @@ class Transpiler {
134
135
  }
135
136
 
136
137
  // ===========================================================================
137
- // Public API: run() and transpileSource()
138
+ // Public API
138
139
  // ===========================================================================
139
140
 
140
141
  /**
141
- * Execute the unified pipeline from CLI inputs.
142
+ * Unified entry point for all transpilation.
142
143
  *
143
- * Stage 1 (file discovery) happens here, then delegates to _executePipeline().
144
+ * @param input - What to transpile:
145
+ * - { kind: 'files' } — discover from config.inputs, write to disk
146
+ * - { kind: 'source', source, ... } — transpile in-memory source
147
+ * @returns ITranspilerResult with per-file results in .files[]
144
148
  */
145
- async run(): Promise<ITranspilerResult> {
149
+ async transpile(input: TTranspileInput): Promise<ITranspilerResult> {
146
150
  const result = this._initResult();
147
151
 
148
152
  try {
149
153
  await this._initializeRun();
150
154
 
151
- // Stage 1: Discover source files
152
- const { cnextFiles, headerFiles } = await this.discoverSources();
153
- if (cnextFiles.length === 0) {
155
+ const pipelineInput = await this.discoverIncludes(input);
156
+ if (pipelineInput.cnextFiles.length === 0) {
154
157
  return this._finalizeResult(result, "No C-Next source files found");
155
158
  }
156
159
 
157
- this._ensureOutputDirectories();
158
-
159
- // Convert IDiscoveredFile[] to IPipelineFile[] (disk-based, all get code gen)
160
- const pipelineFiles: IPipelineFile[] = cnextFiles.map((f) => ({
161
- path: f.path,
162
- discoveredFile: f,
163
- }));
164
-
165
- const input: IPipelineInput = {
166
- cnextFiles: pipelineFiles,
167
- headerFiles,
168
- writeOutputToDisk: true,
169
- };
170
-
171
- await this._executePipeline(input, result);
160
+ if (input.kind === "files") {
161
+ this._ensureOutputDirectories();
162
+ }
172
163
 
164
+ await this._executePipeline(pipelineInput, result);
173
165
  return await this._finalizeResult(result);
174
166
  } catch (err) {
175
167
  return this._handleRunError(result, err);
@@ -177,66 +169,27 @@ class Transpiler {
177
169
  }
178
170
 
179
171
  /**
180
- * Transpile source code provided as a string.
172
+ * Stage 1: Discover files and build pipeline input.
181
173
  *
182
- * Discovers includes from the source, builds an IPipelineInput, and
183
- * delegates to the same _executePipeline() as run().
174
+ * Branches on input kind:
175
+ * - 'files': filesystem scan, dependency graph, topological sort
176
+ * - 'source': parse in-memory string, walk include tree
184
177
  *
185
- * @param source - The C-Next source code as a string
186
- * @param options - Options for transpilation
187
- * @returns Promise<IFileResult> with generated code or errors
178
+ * Header directive storage happens via IncludeResolver.resolve() for both
179
+ * C headers and cnext includes (Issue #854).
188
180
  */
189
- async transpileSource(
190
- source: string,
191
- options?: {
192
- workingDir?: string;
193
- includeDirs?: string[];
194
- sourcePath?: string;
195
- },
196
- ): Promise<IFileResult> {
197
- const workingDir = options?.workingDir ?? process.cwd();
198
- const additionalIncludeDirs = options?.includeDirs ?? [];
199
- const sourcePath = options?.sourcePath ?? "<string>";
200
-
201
- try {
202
- await this._initializeRun();
203
-
204
- const input = this._discoverFromSource(
205
- source,
206
- workingDir,
207
- additionalIncludeDirs,
208
- sourcePath,
209
- );
210
-
211
- const result = this._initResult();
212
- await this._executePipeline(input, result);
213
-
214
- // Find our main file's result
215
- const fileResult = result.files.find((f) => f.sourcePath === sourcePath);
216
- if (fileResult) {
217
- return fileResult;
218
- }
219
-
220
- // No file result found — pipeline exited early (e.g., parse errors in Stage 3)
221
- // Return pipeline errors as a file result
222
- if (result.errors.length > 0) {
223
- return this.buildErrorResult(sourcePath, result.errors, 0);
224
- }
225
- return this.buildErrorResult(
226
- sourcePath,
227
- [
228
- {
229
- line: 1,
230
- column: 0,
231
- message: "Pipeline produced no result for source file",
232
- severity: "error",
233
- },
234
- ],
235
- 0,
236
- );
237
- } catch (err) {
238
- return this.buildCatchResult(sourcePath, err);
181
+ private async discoverIncludes(
182
+ input: TTranspileInput,
183
+ ): Promise<IPipelineInput> {
184
+ if (input.kind === "files") {
185
+ return this._discoverFromFiles();
239
186
  }
187
+ return this._discoverFromSource(
188
+ input.source,
189
+ input.workingDir ?? process.cwd(),
190
+ input.includeDirs ?? [],
191
+ input.sourcePath ?? "<string>",
192
+ );
240
193
  }
241
194
 
242
195
  // ===========================================================================
@@ -246,7 +199,7 @@ class Transpiler {
246
199
  /**
247
200
  * The single unified pipeline for all transpilation.
248
201
  *
249
- * Both run() and transpileSource() delegate here after file discovery.
202
+ * transpile() delegates here after file discovery via discoverIncludes().
250
203
  *
251
204
  * Stage 2: Collect symbols from C/C++ headers (includes building analyzer context)
252
205
  * Stage 3: Collect symbols from C-Next files
@@ -291,7 +244,7 @@ class Transpiler {
291
244
  );
292
245
  }
293
246
 
294
- // Stage 6: Generate headers (only write to disk in run() mode)
247
+ // Stage 6: Generate headers (only write to disk in files mode)
295
248
  if (result.success && input.writeOutputToDisk) {
296
249
  this._generateAllHeadersFromPipeline(input.cnextFiles, result);
297
250
  }
@@ -395,17 +348,7 @@ class Transpiler {
395
348
  return this.buildParseOnlyResult(sourcePath, declarationCount);
396
349
  }
397
350
 
398
- // Run analyzers (reads externalStructFields and symbolTable from CodeGenState)
399
- const analyzerErrors = runAnalyzers(tree, tokenStream);
400
- if (analyzerErrors.length > 0) {
401
- return this.buildErrorResult(
402
- sourcePath,
403
- analyzerErrors,
404
- declarationCount,
405
- );
406
- }
407
-
408
- // Build symbolInfo for code generation
351
+ // Build symbolInfo for code generation (before analyzers so they can read it)
409
352
  const tSymbols = CNextResolver.resolve(tree, sourcePath);
410
353
  let symbolInfo = TSymbolInfoAdapter.convert(tSymbols);
411
354
 
@@ -421,6 +364,19 @@ class Transpiler {
421
364
  );
422
365
  }
423
366
 
367
+ // Make symbols available to analyzers (CodeGenerator.generate() sets this too)
368
+ CodeGenState.symbols = symbolInfo;
369
+
370
+ // Run analyzers (reads symbols, externalStructFields, and symbolTable from CodeGenState)
371
+ const analyzerErrors = runAnalyzers(tree, tokenStream);
372
+ if (analyzerErrors.length > 0) {
373
+ return this.buildErrorResult(
374
+ sourcePath,
375
+ analyzerErrors,
376
+ declarationCount,
377
+ );
378
+ }
379
+
424
380
  // Inject cross-file modification data for const inference
425
381
  this._setupCrossFileModifications();
426
382
 
@@ -450,19 +406,8 @@ class Transpiler {
450
406
  this._accumulateFileModifications();
451
407
  }
452
408
 
453
- // ADR-055 Phase 7: Get TSymbols for header generation (auto-const applied during conversion)
454
- const fileSymbols =
455
- CodeGenState.symbolTable.getTSymbolsByFile(sourcePath);
456
-
457
- // Generate header content
458
- const headerCode = this.generateHeaderContent(
459
- fileSymbols,
460
- sourcePath,
461
- this.cppDetected,
462
- userIncludes,
463
- passByValueCopy,
464
- symbolInfo,
465
- );
409
+ // Generate header content (reads from state populated above)
410
+ const headerCode = this.generateHeaderForFile(file) ?? undefined;
466
411
 
467
412
  return this.buildSuccessResult(
468
413
  sourcePath,
@@ -549,6 +494,15 @@ class Transpiler {
549
494
  }
550
495
  }
551
496
 
497
+ // Issue #854: Store header directives for cnext includes
498
+ for (const cnxInclude of resolved.cnextIncludes) {
499
+ const includePath = resolve(cnxInclude.path);
500
+ const directive = resolved.headerIncludeDirectives.get(includePath);
501
+ if (directive) {
502
+ this.state.setHeaderDirective(includePath, directive);
503
+ }
504
+ }
505
+
552
506
  // Walk C-Next includes transitively to build include file list
553
507
  const cnextIncludeFiles: IPipelineFile[] = [];
554
508
  IncludeTreeWalker.walk(
@@ -619,6 +573,9 @@ class Transpiler {
619
573
  CodeGenState.symbolTable.clear();
620
574
  // Reset SymbolRegistry for new run (new IFunctionSymbol type system)
621
575
  SymbolRegistry.reset();
576
+ // Reset callback-compatible functions for new run
577
+ // (populated by FunctionCallAnalyzer, persists through CodeGenState.reset())
578
+ CodeGenState.callbackCompatibleFunctions = new Set();
622
579
  }
623
580
 
624
581
  /**
@@ -721,8 +678,12 @@ class Transpiler {
721
678
  if (file.symbolOnly) {
722
679
  continue;
723
680
  }
724
- const headerPath = this.generateHeader(file.discoveredFile);
725
- if (headerPath) {
681
+ const headerContent = this.generateHeaderForFile(file);
682
+ if (headerContent) {
683
+ const headerPath = this.pathResolver.getHeaderOutputPath(
684
+ file.discoveredFile,
685
+ );
686
+ this.fs.writeFile(headerPath, headerContent);
726
687
  result.outputFiles.push(headerPath);
727
688
  }
728
689
  }
@@ -766,7 +727,7 @@ class Transpiler {
766
727
  }
767
728
 
768
729
  // ===========================================================================
769
- // Source Discovery (Stage 1 for run())
730
+ // File Discovery (Stage 1 for files mode)
770
731
  // ===========================================================================
771
732
 
772
733
  /**
@@ -841,7 +802,10 @@ class Transpiler {
841
802
  * Issue #580: Track dependencies for topological sorting
842
803
  */
843
804
  private _processCnextIncludes(
844
- resolved: { cnextIncludes: IDiscoveredFile[] },
805
+ resolved: {
806
+ cnextIncludes: IDiscoveredFile[];
807
+ headerIncludeDirectives: Map<string, string>;
808
+ },
845
809
  cnxPath: string,
846
810
  depGraph: DependencyGraph,
847
811
  cnextFiles: IDiscoveredFile[],
@@ -857,6 +821,12 @@ class Transpiler {
857
821
 
858
822
  depGraph.addDependency(cnxPath, includePath);
859
823
 
824
+ // Issue #854: Store header directive for cnext include types
825
+ const directive = resolved.headerIncludeDirectives.get(includePath);
826
+ if (directive) {
827
+ this.state.setHeaderDirective(includePath, directive);
828
+ }
829
+
860
830
  // Don't add if already in the list
861
831
  const alreadyExists =
862
832
  cnextBaseNames.has(includeBaseName) ||
@@ -944,10 +914,7 @@ class Transpiler {
944
914
  * This ensures headers are found based on what the source actually
945
915
  * includes, not by blindly scanning include directories.
946
916
  */
947
- private async discoverSources(): Promise<{
948
- cnextFiles: IDiscoveredFile[];
949
- headerFiles: IDiscoveredFile[];
950
- }> {
917
+ private async _discoverFromFiles(): Promise<IPipelineInput> {
951
918
  // Step 1: Discover C-Next files from inputs (files or directories)
952
919
  const cnextFiles: IDiscoveredFile[] = [];
953
920
  const fileByPath = new Map<string, IDiscoveredFile>();
@@ -957,7 +924,7 @@ class Transpiler {
957
924
  }
958
925
 
959
926
  if (cnextFiles.length === 0) {
960
- return { cnextFiles: [], headerFiles: [] };
927
+ return { cnextFiles: [], headerFiles: [], writeOutputToDisk: true };
961
928
  }
962
929
 
963
930
  // Step 2: For each C-Next file, resolve its #include directives
@@ -981,7 +948,7 @@ class Transpiler {
981
948
  // Issue #580: Sort files topologically for correct cross-file const inference
982
949
  const sortedCnextFiles = this._sortFilesByDependency(depGraph, fileByPath);
983
950
 
984
- // Resolve headers transitively for the run() path
951
+ // Resolve headers transitively
985
952
  const { headers: allHeaders, warnings: headerWarnings } =
986
953
  IncludeResolver.resolveHeadersTransitively(
987
954
  [...headerSet.values()],
@@ -996,9 +963,16 @@ class Transpiler {
996
963
  );
997
964
  this.warnings.push(...headerWarnings);
998
965
 
966
+ // Convert IDiscoveredFile[] to IPipelineFile[] (disk-based, all get code gen)
967
+ const pipelineFiles: IPipelineFile[] = sortedCnextFiles.map((f) => ({
968
+ path: f.path,
969
+ discoveredFile: f,
970
+ }));
971
+
999
972
  return {
1000
- cnextFiles: sortedCnextFiles,
973
+ cnextFiles: pipelineFiles,
1001
974
  headerFiles: allHeaders,
975
+ writeOutputToDisk: true,
1002
976
  };
1003
977
  }
1004
978
 
@@ -1242,48 +1216,41 @@ class Transpiler {
1242
1216
  * Stage 6: Generate header file for a C-Next file
1243
1217
  * ADR-055 Phase 7: Uses TSymbol directly, converts to IHeaderSymbol for generation.
1244
1218
  */
1245
- private generateHeader(file: IDiscoveredFile): string | null {
1246
- const tSymbols = CodeGenState.symbolTable.getTSymbolsByFile(file.path);
1219
+ /**
1220
+ * Generate header content for a single file's exported symbols.
1221
+ * Unified method replacing both generateHeader() and generateHeaderContent().
1222
+ * Reads all needed data from state (populated during Stage 5).
1223
+ */
1224
+ private generateHeaderForFile(file: IPipelineFile): string | null {
1225
+ const sourcePath = file.path;
1226
+ const tSymbols = CodeGenState.symbolTable.getTSymbolsByFile(sourcePath);
1247
1227
  const exportedSymbols = tSymbols.filter((s) => s.isExported);
1248
1228
 
1249
1229
  if (exportedSymbols.length === 0) {
1250
1230
  return null;
1251
1231
  }
1252
1232
 
1253
- const headerName = basename(file.path).replace(/\.cnx$|\.cnext$/, ".h");
1254
- const headerPath = this.pathResolver.getHeaderOutputPath(file);
1255
-
1256
- // Issue #220: Get SymbolCollector for full type definitions
1257
- const typeInput = this.state.getSymbolInfo(file.path);
1233
+ const headerName = basename(sourcePath).replace(/\.cnx$|\.cnext$/, ".h");
1258
1234
 
1259
- // Issue #280: Get pass-by-value params from per-file storage for multi-file consistency
1260
- // This uses the snapshot taken during transpilation, not the current (stale) codeGenerator state.
1261
- // Fallback to empty map if not found (defensive - should always exist after transpilation).
1235
+ const typeInput = this.state.getSymbolInfo(sourcePath);
1262
1236
  const passByValueParams =
1263
- this.state.getPassByValueParams(file.path) ??
1237
+ this.state.getPassByValueParams(sourcePath) ??
1264
1238
  new Map<string, Set<string>>();
1239
+ const userIncludes = this.state.getUserIncludes(sourcePath);
1265
1240
 
1266
- // Issue #424: Get user includes for header generation
1267
- const userIncludes = this.state.getUserIncludes(file.path);
1268
-
1269
- // Issue #478, #588: Collect all known enum names from all files for cross-file type handling
1270
1241
  const allKnownEnums = TransitiveEnumCollector.aggregateKnownEnums(
1271
1242
  this.state.getAllSymbolInfo(),
1272
1243
  );
1273
1244
 
1274
- // Issue #497: Build mapping from external types to their C header includes
1275
1245
  const externalTypeHeaders = ExternalTypeHeaderBuilder.build(
1276
1246
  this.state.getAllHeaderDirectives(),
1277
1247
  CodeGenState.symbolTable,
1278
1248
  );
1279
1249
 
1280
- // Issue #502: Include symbolTable in typeInput for C++ namespace type detection
1281
1250
  const typeInputWithSymbolTable = typeInput
1282
1251
  ? { ...typeInput, symbolTable: CodeGenState.symbolTable }
1283
1252
  : undefined;
1284
1253
 
1285
- // ADR-055 Phase 7: Convert TSymbol to IHeaderSymbol with auto-const info
1286
- // Issue #817: Apply auto-const info same as generateHeaderContent() does
1287
1254
  const unmodifiedParams = this.codeGenerator.getFunctionUnmodifiedParams();
1288
1255
  const headerSymbols = this.convertToHeaderSymbols(
1289
1256
  exportedSymbols,
@@ -1291,7 +1258,7 @@ class Transpiler {
1291
1258
  allKnownEnums,
1292
1259
  );
1293
1260
 
1294
- const headerContent = this.headerGenerator.generate(
1261
+ return this.headerGenerator.generate(
1295
1262
  headerSymbols,
1296
1263
  headerName,
1297
1264
  {
@@ -1304,9 +1271,6 @@ class Transpiler {
1304
1271
  passByValueParams,
1305
1272
  allKnownEnums,
1306
1273
  );
1307
-
1308
- this.fs.writeFile(headerPath, headerContent);
1309
- return headerPath;
1310
1274
  }
1311
1275
 
1312
1276
  /**
@@ -1351,65 +1315,6 @@ class Transpiler {
1351
1315
  }
1352
1316
  }
1353
1317
 
1354
- /**
1355
- * Generate header content for exported symbols.
1356
- * Issue #591: Extracted from transpileSource() for reduced complexity.
1357
- * ADR-055 Phase 7: Works with TSymbol[], converts to IHeaderSymbol for generation.
1358
- */
1359
- private generateHeaderContent(
1360
- tSymbols: TSymbol[],
1361
- sourcePath: string,
1362
- cppMode: boolean,
1363
- userIncludes: string[],
1364
- passByValueParams: Map<string, Set<string>>,
1365
- symbolInfo: ICodeGenSymbols,
1366
- ): string | undefined {
1367
- const exportedSymbols = tSymbols.filter((s) => s.isExported);
1368
-
1369
- if (exportedSymbols.length === 0) {
1370
- return undefined;
1371
- }
1372
-
1373
- // Convert to IHeaderSymbol with auto-const info
1374
- const unmodifiedParams = this.codeGenerator.getFunctionUnmodifiedParams();
1375
- const headerSymbols = this.convertToHeaderSymbols(
1376
- exportedSymbols,
1377
- unmodifiedParams,
1378
- symbolInfo.knownEnums,
1379
- );
1380
-
1381
- const headerName = basename(sourcePath).replace(/\.cnx$|\.cnext$/, ".h");
1382
-
1383
- // Get type input from CodeGenState (for struct/enum definitions)
1384
- const typeInput = CodeGenState.symbols;
1385
-
1386
- // Issue #497: Build mapping from external types to their C header includes
1387
- const externalTypeHeaders = ExternalTypeHeaderBuilder.build(
1388
- this.state.getAllHeaderDirectives(),
1389
- CodeGenState.symbolTable,
1390
- );
1391
-
1392
- // Issue #502: Include symbolTable in typeInput for C++ namespace type detection
1393
- const typeInputWithSymbolTable = typeInput
1394
- ? { ...typeInput, symbolTable: CodeGenState.symbolTable }
1395
- : undefined;
1396
-
1397
- // Issue #478: Pass all known enums for cross-file type handling
1398
- return this.headerGenerator.generate(
1399
- headerSymbols,
1400
- headerName,
1401
- {
1402
- exportedOnly: true,
1403
- userIncludes,
1404
- externalTypeHeaders,
1405
- cppMode,
1406
- },
1407
- typeInputWithSymbolTable,
1408
- passByValueParams,
1409
- symbolInfo.knownEnums,
1410
- );
1411
- }
1412
-
1413
1318
  /**
1414
1319
  * Convert TSymbols to IHeaderSymbols with auto-const information applied.
1415
1320
  * ADR-055 Phase 7: Replaces mutation-based auto-const updating.