c-next 0.1.21 → 0.1.23

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/README.md CHANGED
@@ -77,6 +77,12 @@ cnext examples/blink.cnx --cpp
77
77
  # Target platform for atomic code generation (ADR-049)
78
78
  cnext examples/blink.cnx --target teensy41
79
79
 
80
+ # Separate output directories for code and headers
81
+ cnext src/ -o build/src --header-out build/include
82
+
83
+ # Clean generated files
84
+ cnext src/ -o build/src --header-out build/include --clean
85
+
80
86
  # Show all options
81
87
  cnext --help
82
88
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -0,0 +1,148 @@
1
+ /**
2
+ * CleanCommand
3
+ * Deletes generated files (.c, .cpp, .h, .hpp) that have matching .cnx sources
4
+ */
5
+
6
+ import { basename, join, relative, resolve } from "path";
7
+ import { existsSync, statSync, unlinkSync } from "fs";
8
+ import InputExpansion from "../lib/InputExpansion";
9
+
10
+ /**
11
+ * Command to clean generated output files
12
+ */
13
+ class CleanCommand {
14
+ /**
15
+ * Execute the clean command
16
+ *
17
+ * @param inputs - Input files or directories (source locations)
18
+ * @param outDir - Output directory for code files
19
+ * @param headerOutDir - Optional separate output directory for headers
20
+ */
21
+ static execute(
22
+ inputs: string[],
23
+ outDir: string,
24
+ headerOutDir?: string,
25
+ ): void {
26
+ // If no outDir specified, we can't determine where to clean
27
+ if (!outDir) {
28
+ console.log("No output directory specified. Nothing to clean.");
29
+ return;
30
+ }
31
+
32
+ // Discover all .cnx files
33
+ let cnxFiles: string[];
34
+ try {
35
+ cnxFiles = InputExpansion.expandInputs(inputs);
36
+ } catch (error) {
37
+ console.error(`Error: ${error}`);
38
+ return;
39
+ }
40
+
41
+ if (cnxFiles.length === 0) {
42
+ console.log("No .cnx files found. Nothing to clean.");
43
+ return;
44
+ }
45
+
46
+ const resolvedOutDir = resolve(outDir);
47
+ const resolvedHeaderDir = headerOutDir
48
+ ? resolve(headerOutDir)
49
+ : resolvedOutDir;
50
+
51
+ let deletedCount = 0;
52
+
53
+ // For each .cnx file, calculate and delete generated files
54
+ for (const cnxFile of cnxFiles) {
55
+ const baseName = basename(cnxFile).replace(/\.cnx$|\.cnext$/, "");
56
+
57
+ // Calculate relative path from input directories
58
+ const relativePath = this.getRelativePath(cnxFile, inputs);
59
+
60
+ // Code files (.c and .cpp) go to outDir
61
+ const codeExtensions = [".c", ".cpp"];
62
+ for (const ext of codeExtensions) {
63
+ const outputPath = relativePath
64
+ ? join(resolvedOutDir, relativePath.replace(/\.cnx$|\.cnext$/, ext))
65
+ : join(resolvedOutDir, baseName + ext);
66
+
67
+ if (this.deleteIfExists(outputPath)) {
68
+ deletedCount++;
69
+ }
70
+ }
71
+
72
+ // Header files (.h and .hpp) go to headerOutDir
73
+ const headerExtensions = [".h", ".hpp"];
74
+ for (const ext of headerExtensions) {
75
+ const headerPath = relativePath
76
+ ? join(
77
+ resolvedHeaderDir,
78
+ relativePath.replace(/\.cnx$|\.cnext$/, ext),
79
+ )
80
+ : join(resolvedHeaderDir, baseName + ext);
81
+
82
+ if (this.deleteIfExists(headerPath)) {
83
+ deletedCount++;
84
+ }
85
+ }
86
+ }
87
+
88
+ if (deletedCount === 0) {
89
+ console.log("No generated files found to delete.");
90
+ } else {
91
+ console.log(`Deleted ${deletedCount} generated file(s).`);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get relative path of a file from input directories.
97
+ * Returns undefined if file is not under any input directory.
98
+ *
99
+ * When undefined is returned (e.g., for single file inputs), the caller
100
+ * falls back to using just the basename, which is correct behavior since
101
+ * there's no directory structure to preserve.
102
+ */
103
+ private static getRelativePath(
104
+ filePath: string,
105
+ inputs: string[],
106
+ ): string | undefined {
107
+ for (const input of inputs) {
108
+ const resolvedInput = resolve(input);
109
+
110
+ // Skip file inputs - only directories can establish relative structure.
111
+ // For single file inputs like "cnext myfile.cnx -o build", we return
112
+ // undefined and the caller uses baseName, which is the correct behavior.
113
+ if (existsSync(resolvedInput) && statSync(resolvedInput).isFile()) {
114
+ continue;
115
+ }
116
+
117
+ const rel = relative(resolvedInput, filePath);
118
+
119
+ // Check if file is under this input directory
120
+ if (rel && !rel.startsWith("..")) {
121
+ return rel;
122
+ }
123
+ }
124
+
125
+ return undefined;
126
+ }
127
+
128
+ /**
129
+ * Delete a file if it exists
130
+ * @returns true if file was deleted, false otherwise
131
+ */
132
+ private static deleteIfExists(filePath: string): boolean {
133
+ try {
134
+ unlinkSync(filePath);
135
+ console.log(` Deleted: ${filePath}`);
136
+ return true;
137
+ } catch (err: unknown) {
138
+ // ENOENT means file doesn't exist - not an error for our purposes
139
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
140
+ return false;
141
+ }
142
+ console.error(` Failed to delete ${filePath}: ${err}`);
143
+ return false;
144
+ }
145
+ }
146
+ }
147
+
148
+ export default CleanCommand;
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * A safer C for embedded systems development
5
5
  */
6
6
 
7
+ import CleanCommand from "./commands/CleanCommand";
7
8
  import IncludeDiscovery from "./lib/IncludeDiscovery";
8
9
  import InputExpansion from "./lib/InputExpansion";
9
10
  import Project from "./project/Project";
@@ -107,6 +108,10 @@ function showHelp(): void {
107
108
  );
108
109
  console.log(" --no-preprocess Don't run C preprocessor on headers");
109
110
  console.log(" --no-cache Disable symbol cache (.cnx/ directory)");
111
+ console.log(" --header-out <dir> Output directory for header files");
112
+ console.log(
113
+ " --clean Delete generated files for all .cnx sources",
114
+ );
110
115
  console.log(" -D<name>[=value] Define preprocessor macro");
111
116
  console.log(" --pio-install Setup PlatformIO integration");
112
117
  console.log(" --pio-uninstall Remove PlatformIO integration");
@@ -191,6 +196,7 @@ async function runUnifiedMode(
191
196
  verbose: boolean,
192
197
  cppRequired: boolean,
193
198
  noCache: boolean,
199
+ headerOutDir?: string,
194
200
  ): Promise<void> {
195
201
  // Step 1: Expand directories to .cnx files
196
202
  let files: string[];
@@ -255,6 +261,7 @@ async function runUnifiedMode(
255
261
  files,
256
262
  includeDirs: allIncludePaths,
257
263
  outDir,
264
+ headerOutDir,
258
265
  generateHeaders,
259
266
  preprocess,
260
267
  defines,
@@ -471,6 +478,8 @@ async function main(): Promise<void> {
471
478
  let preprocess = true;
472
479
  let verbose = false;
473
480
  let noCache = false;
481
+ let headerOutDir: string | undefined;
482
+ let cleanMode = false;
474
483
 
475
484
  for (let i = 0; i < args.length; i++) {
476
485
  const arg = args[i];
@@ -489,6 +498,10 @@ async function main(): Promise<void> {
489
498
  preprocess = false;
490
499
  } else if (arg === "--no-cache") {
491
500
  noCache = true;
501
+ } else if (arg === "--header-out" && i + 1 < args.length) {
502
+ headerOutDir = args[++i];
503
+ } else if (arg === "--clean") {
504
+ cleanMode = true;
492
505
  } else if (arg.startsWith("-D")) {
493
506
  const define = arg.slice(2);
494
507
  const eqIndex = define.indexOf("=");
@@ -518,6 +531,12 @@ async function main(): Promise<void> {
518
531
  process.exit(1);
519
532
  }
520
533
 
534
+ // Clean mode: delete generated files and exit
535
+ if (cleanMode) {
536
+ CleanCommand.execute(inputFiles, outputPath, headerOutDir);
537
+ process.exit(0);
538
+ }
539
+
521
540
  await runUnifiedMode(
522
541
  inputFiles,
523
542
  outputPath,
@@ -528,6 +547,7 @@ async function main(): Promise<void> {
528
547
  verbose,
529
548
  cppRequired,
530
549
  noCache,
550
+ headerOutDir,
531
551
  );
532
552
  }
533
553
 
@@ -76,6 +76,7 @@ class Pipeline {
76
76
  inputs: config.inputs,
77
77
  includeDirs: config.includeDirs ?? [],
78
78
  outDir: config.outDir ?? "",
79
+ headerOutDir: config.headerOutDir ?? "",
79
80
  defines: config.defines ?? {},
80
81
  preprocess: config.preprocess ?? true,
81
82
  generateHeaders: config.generateHeaders ?? true,
@@ -137,6 +138,11 @@ class Pipeline {
137
138
  mkdirSync(this.config.outDir, { recursive: true });
138
139
  }
139
140
 
141
+ // Ensure header output directory exists if specified separately
142
+ if (this.config.headerOutDir && !existsSync(this.config.headerOutDir)) {
143
+ mkdirSync(this.config.headerOutDir, { recursive: true });
144
+ }
145
+
140
146
  // Stage 2: Collect symbols from C/C++ headers
141
147
  for (const file of headerFiles) {
142
148
  try {
@@ -363,6 +369,17 @@ class Pipeline {
363
369
  // The preprocessor expands/removes #include directives, so we need the original
364
370
  const originalContent = readFileSync(file.path, "utf-8");
365
371
 
372
+ // Issue #328: Skip headers generated by C-Next Transpiler
373
+ // During incremental migration, generated .h files may be discovered via #include
374
+ // recursion from C++ files. These headers contain the same symbols as their .cnx
375
+ // source files, so including them would cause false symbol conflicts.
376
+ if (originalContent.includes("Generated by C-Next Transpiler")) {
377
+ if (this.config.debugMode) {
378
+ console.log(`[DEBUG] Skipping C-Next generated header: ${file.path}`);
379
+ }
380
+ return;
381
+ }
382
+
366
383
  // Issue #321: Recursively process #include directives in headers
367
384
  // This ensures symbols from nested headers (like Arduino's extern HardwareSerial Serial)
368
385
  // are properly collected even when included transitively
@@ -537,7 +554,9 @@ class Pipeline {
537
554
  throw new Error(errors.join("\n"));
538
555
  }
539
556
 
540
- const collector = new CNextSymbolCollector(file.path);
557
+ // Issue #332: Pass symbolTable to collector so struct fields are registered
558
+ // This enables TypeResolver.isStructType() to identify C-Next structs from included files
559
+ const collector = new CNextSymbolCollector(file.path, this.symbolTable);
541
560
  const symbols = collector.collect(tree);
542
561
  this.symbolTable.addSymbols(symbols);
543
562
  }
@@ -794,10 +813,14 @@ class Pipeline {
794
813
 
795
814
  /**
796
815
  * Get output path for a header file
816
+ * Uses headerOutDir if specified, otherwise falls back to outDir
797
817
  */
798
818
  private getHeaderOutputPath(file: IDiscoveredFile): string {
799
819
  const headerName = basename(file.path).replace(/\.cnx$|\.cnext$/, ".h");
800
820
 
821
+ // Use headerOutDir if specified, otherwise fall back to outDir
822
+ const headerDir = this.config.headerOutDir || this.config.outDir;
823
+
801
824
  // Check if file is in any input directory (for preserving structure)
802
825
  for (const input of this.config.inputs) {
803
826
  const resolvedInput = resolve(input);
@@ -812,7 +835,7 @@ class Pipeline {
812
835
  // Check if file is under this input directory
813
836
  if (relativePath && !relativePath.startsWith("..")) {
814
837
  const outputRelative = relativePath.replace(/\.cnx$|\.cnext$/, ".h");
815
- const outputPath = join(this.config.outDir, outputRelative);
838
+ const outputPath = join(headerDir, outputRelative);
816
839
 
817
840
  const outputDir = dirname(outputPath);
818
841
  if (!existsSync(outputDir)) {
@@ -823,8 +846,8 @@ class Pipeline {
823
846
  }
824
847
  }
825
848
 
826
- // Fallback: flat output in outDir
827
- return join(this.config.outDir, headerName);
849
+ // Fallback: flat output in headerDir
850
+ return join(headerDir, headerName);
828
851
  }
829
852
 
830
853
  /**
@@ -14,6 +14,9 @@ interface IPipelineConfig {
14
14
  /** Output directory for generated files (defaults to same as input) */
15
15
  outDir?: string;
16
16
 
17
+ /** Separate output directory for header files (defaults to outDir) */
18
+ headerOutDir?: string;
19
+
17
20
  /** Preprocessor defines for C/C++ headers */
18
21
  defines?: Record<string, string | boolean>;
19
22
 
@@ -31,6 +31,9 @@ const EXTENSION_MAP: Record<string, EFileType> = {
31
31
  class FileDiscovery {
32
32
  /**
33
33
  * Discover files in the given directories
34
+ *
35
+ * Issue #331: Uses a Set to track discovered file paths and avoid duplicates
36
+ * when overlapping directories are provided (e.g., both src/Display and src).
34
37
  */
35
38
  static discover(
36
39
  directories: string[],
@@ -44,6 +47,8 @@ class FileDiscovery {
44
47
  /\.build/,
45
48
  /\.pio/,
46
49
  ];
50
+ // Issue #331: Track discovered paths to avoid duplicates from overlapping dirs
51
+ const discoveredPaths = new Set<string>();
47
52
 
48
53
  for (const dir of directories) {
49
54
  const resolvedDir = resolve(dir);
@@ -59,6 +64,7 @@ class FileDiscovery {
59
64
  recursive,
60
65
  options.extensions,
61
66
  excludePatterns,
67
+ discoveredPaths,
62
68
  );
63
69
  }
64
70
 
@@ -131,6 +137,9 @@ class FileDiscovery {
131
137
 
132
138
  /**
133
139
  * Scan a directory for source files
140
+ *
141
+ * Issue #331: discoveredPaths parameter tracks already-discovered files
142
+ * to avoid duplicates when scanning overlapping directories.
134
143
  */
135
144
  private static scanDirectory(
136
145
  dir: string,
@@ -138,6 +147,7 @@ class FileDiscovery {
138
147
  recursive: boolean,
139
148
  extensions: string[] | undefined,
140
149
  excludePatterns: RegExp[],
150
+ discoveredPaths: Set<string>,
141
151
  ): void {
142
152
  let entries: string[];
143
153
 
@@ -171,9 +181,15 @@ class FileDiscovery {
171
181
  recursive,
172
182
  extensions,
173
183
  excludePatterns,
184
+ discoveredPaths,
174
185
  );
175
186
  }
176
187
  } else if (stats.isFile()) {
188
+ // Issue #331: Skip already-discovered files (from overlapping directories)
189
+ if (discoveredPaths.has(fullPath)) {
190
+ continue;
191
+ }
192
+
177
193
  const ext = extname(fullPath).toLowerCase();
178
194
 
179
195
  // Check extension filter
@@ -183,6 +199,7 @@ class FileDiscovery {
183
199
 
184
200
  const type = EXTENSION_MAP[ext];
185
201
  if (type) {
202
+ discoveredPaths.add(fullPath);
186
203
  files.push({
187
204
  path: fullPath,
188
205
  type,
@@ -46,6 +46,7 @@ class Project {
46
46
  inputs,
47
47
  includeDirs: this.config.includeDirs,
48
48
  outDir: this.config.outDir,
49
+ headerOutDir: this.config.headerOutDir,
49
50
  defines: this.config.defines,
50
51
  preprocess: this.config.preprocess,
51
52
  generateHeaders: this.config.generateHeaders,
@@ -14,6 +14,9 @@ interface IProjectConfig {
14
14
  /** Output directory for generated files */
15
15
  outDir: string;
16
16
 
17
+ /** Separate output directory for header files (defaults to outDir) */
18
+ headerOutDir?: string;
19
+
17
20
  /** Specific files to compile (overrides srcDirs) */
18
21
  files?: string[];
19
22
 
@@ -7,6 +7,7 @@ import * as Parser from "../parser/grammar/CNextParser";
7
7
  import ISymbol from "../types/ISymbol";
8
8
  import ESymbolKind from "../types/ESymbolKind";
9
9
  import ESourceLanguage from "../types/ESourceLanguage";
10
+ import SymbolTable from "./SymbolTable";
10
11
 
11
12
  /**
12
13
  * Collects symbols from a C-Next parse tree
@@ -16,8 +17,12 @@ class CNextSymbolCollector {
16
17
 
17
18
  private symbols: ISymbol[] = [];
18
19
 
19
- constructor(sourceFile: string) {
20
+ // Issue #332: Optional SymbolTable to register struct fields for isStructType() lookup
21
+ private symbolTable: SymbolTable | null;
22
+
23
+ constructor(sourceFile: string, symbolTable?: SymbolTable) {
20
24
  this.sourceFile = sourceFile;
25
+ this.symbolTable = symbolTable ?? null;
21
26
  }
22
27
 
23
28
  /**
@@ -166,6 +171,45 @@ class CNextSymbolCollector {
166
171
  sourceLanguage: ESourceLanguage.CNext,
167
172
  isExported: true,
168
173
  });
174
+
175
+ // Issue #332: Register struct fields in SymbolTable for isStructType() lookup
176
+ // This enables TypeResolver to identify C-Next structs from included files
177
+ // when determining if & should be added for pointer parameters
178
+ if (this.symbolTable) {
179
+ for (const member of struct.structMember()) {
180
+ const fieldName = member.IDENTIFIER().getText();
181
+ const fieldType = this.getTypeText(member.type());
182
+
183
+ // Check for array dimensions
184
+ const arrayDims = member.arrayDimension();
185
+ const dimensions: number[] = [];
186
+ for (const dim of arrayDims) {
187
+ const sizeExpr = dim.expression();
188
+ if (sizeExpr) {
189
+ const size = parseInt(sizeExpr.getText(), 10);
190
+ if (!isNaN(size)) {
191
+ dimensions.push(size);
192
+ }
193
+ }
194
+ }
195
+
196
+ this.symbolTable!.addStructField(
197
+ name,
198
+ fieldName,
199
+ fieldType,
200
+ dimensions.length > 0 ? dimensions : undefined,
201
+ );
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Issue #332: Extract type text from a type context
208
+ * Simple extraction for struct field registration
209
+ */
210
+ private getTypeText(ctx: Parser.TypeContext | null): string {
211
+ if (!ctx) return "void";
212
+ return ctx.getText();
169
213
  }
170
214
 
171
215
  private collectRegister(reg: Parser.RegisterDeclarationContext): void {