c-next 0.1.24 → 0.1.26

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.24",
3
+ "version": "0.1.26",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -255,6 +255,12 @@ export default class CodeGenerator implements IOrchestrator {
255
255
  /** ADR-010: Source file path for validating includes */
256
256
  private sourcePath: string | null = null;
257
257
 
258
+ /** Issue #349: Include directories for resolving angle-bracket .cnx includes */
259
+ private includeDirs: string[] = [];
260
+
261
+ /** Issue #349: Input directories for calculating relative paths */
262
+ private inputs: string[] = [];
263
+
258
264
  /** Token stream for comment extraction (ADR-043) */
259
265
  private tokenStream: CommonTokenStream | null = null;
260
266
 
@@ -1309,6 +1315,10 @@ export default class CodeGenerator implements IOrchestrator {
1309
1315
  // ADR-010: Store source path for include validation
1310
1316
  this.sourcePath = options?.sourcePath ?? null;
1311
1317
 
1318
+ // Issue #349: Store include directories and inputs for angle-bracket include resolution
1319
+ this.includeDirs = options?.includeDirs ?? [];
1320
+ this.inputs = options?.inputs ?? [];
1321
+
1312
1322
  // Issue #250: Store C++ mode for temp variable generation
1313
1323
  this.cppMode = options?.cppMode ?? false;
1314
1324
  // Reset temp var state for each generation
@@ -1430,13 +1440,17 @@ export default class CodeGenerator implements IOrchestrator {
1430
1440
  // Issue #230: Self-include for extern "C" linkage
1431
1441
  // When file has public symbols and headers are being generated,
1432
1442
  // include own header to ensure proper C linkage
1443
+ // Issue #339: Use relative path from source root when available
1433
1444
  if (
1434
1445
  options?.generateHeaders &&
1435
1446
  this.symbols!.hasPublicSymbols() &&
1436
1447
  this.sourcePath
1437
1448
  ) {
1438
- const basename = this.sourcePath.replace(/^.*[\\/]/, "");
1439
- const headerName = basename.replace(/\.cnx$|\.cnext$/, ".h");
1449
+ // Issue #339: Prefer sourceRelativePath for correct directory structure
1450
+ // Otherwise fall back to basename for backward compatibility
1451
+ const pathToUse =
1452
+ options.sourceRelativePath || this.sourcePath.replace(/^.*[\\/]/, "");
1453
+ const headerName = pathToUse.replace(/\.cnx$|\.cnext$/, ".h");
1440
1454
  output.push(`#include "${headerName}"`);
1441
1455
  output.push("");
1442
1456
  }
@@ -1613,9 +1627,14 @@ export default class CodeGenerator implements IOrchestrator {
1613
1627
  /**
1614
1628
  * ADR-010: Transform #include directives, converting .cnx to .h
1615
1629
  * ADR-053 A5: Delegates to IncludeGenerator
1630
+ * Issue #349: Now passes includeDirs and inputs for angle-bracket resolution
1616
1631
  */
1617
1632
  private transformIncludeDirective(includeText: string): string {
1618
- return includeTransformIncludeDirective(includeText, this.sourcePath);
1633
+ return includeTransformIncludeDirective(includeText, {
1634
+ sourcePath: this.sourcePath,
1635
+ includeDirs: this.includeDirs,
1636
+ inputs: this.inputs,
1637
+ });
1619
1638
  }
1620
1639
 
1621
1640
  // Issue #63: validateIncludeNotImplementationFile moved to TypeValidator
@@ -268,7 +268,9 @@ class HeaderGenerator {
268
268
  passByValueParams?: TPassByValueParams,
269
269
  ): string | null {
270
270
  // Map return type from C-Next to C
271
- const returnType = sym.type ? mapType(sym.type) : "void";
271
+ // Special case: main() always returns int for C/C++ compatibility
272
+ const mappedType = sym.type ? mapType(sym.type) : "void";
273
+ const returnType = sym.name === "main" ? "int" : mappedType;
272
274
 
273
275
  // Issue #269: Get pass-by-value parameter names for this function
274
276
  const passByValueSet = passByValueParams?.get(sym.name);
@@ -6,22 +6,98 @@ import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import * as Parser from "../../../parser/grammar/CNextParser";
8
8
 
9
+ /**
10
+ * Issue #349: Options for include transformation
11
+ */
12
+ interface IIncludeTransformOptions {
13
+ sourcePath: string | null;
14
+ includeDirs?: string[];
15
+ inputs?: string[];
16
+ }
17
+
18
+ /**
19
+ * Issue #349: Find a .cnx file in the given search paths.
20
+ * Returns the absolute path if found, null otherwise.
21
+ */
22
+ const findCnxFile = (
23
+ filename: string,
24
+ searchPaths: string[],
25
+ ): string | null => {
26
+ for (const searchPath of searchPaths) {
27
+ const cnxPath = path.resolve(searchPath, `${filename}.cnx`);
28
+ if (fs.existsSync(cnxPath)) {
29
+ return cnxPath;
30
+ }
31
+ }
32
+ return null;
33
+ };
34
+
35
+ /**
36
+ * Issue #349: Calculate the relative path from input directories.
37
+ * Returns the relative path (e.g., "Display/utils.cnx") or null if not found.
38
+ */
39
+ const getRelativePathFromInputs = (
40
+ filePath: string,
41
+ inputs: string[],
42
+ ): string | null => {
43
+ for (const input of inputs) {
44
+ const resolvedInput = path.resolve(input);
45
+
46
+ // Skip if input is a file (not a directory)
47
+ if (fs.existsSync(resolvedInput) && fs.statSync(resolvedInput).isFile()) {
48
+ continue;
49
+ }
50
+
51
+ const relativePath = path.relative(resolvedInput, filePath);
52
+
53
+ // If relative path doesn't start with '..' it's under this input
54
+ if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
55
+ return relativePath;
56
+ }
57
+ }
58
+ return null;
59
+ };
60
+
9
61
  /**
10
62
  * ADR-010: Transform #include directives, converting .cnx to .h
11
63
  * Validates that .cnx files exist if sourcePath is available
12
64
  * Supports both <file.cnx> and "file.cnx" forms
65
+ *
66
+ * Issue #349: For angle-bracket includes, resolves the correct output path
67
+ * by finding the .cnx file and calculating its relative path from inputs.
13
68
  */
14
69
  const transformIncludeDirective = (
15
70
  includeText: string,
16
- sourcePath: string | null,
71
+ options: IIncludeTransformOptions,
17
72
  ): string => {
73
+ const { sourcePath, includeDirs = [], inputs = [] } = options;
74
+
18
75
  // Match: #include <file.cnx> or #include "file.cnx"
19
76
  const angleMatch = includeText.match(/#\s*include\s*<([^>]+)\.cnx>/);
20
77
  const quoteMatch = includeText.match(/#\s*include\s*"([^"]+)\.cnx"/);
21
78
 
22
79
  if (angleMatch) {
23
80
  const filename = angleMatch[1];
24
- // Angle brackets: system/library includes - no validation needed
81
+
82
+ // Issue #349: Try to resolve the .cnx file to get correct output path
83
+ if (sourcePath) {
84
+ const sourceDir = path.dirname(sourcePath);
85
+ // Build search paths: source directory first, then include directories
86
+ const searchPaths = [sourceDir, ...includeDirs];
87
+
88
+ const foundPath = findCnxFile(filename, searchPaths);
89
+ if (foundPath && inputs.length > 0) {
90
+ // Calculate relative path from inputs for correct header path
91
+ const relativePath = getRelativePathFromInputs(foundPath, inputs);
92
+ if (relativePath) {
93
+ // Transform .cnx to .h
94
+ const headerPath = relativePath.replace(/\.cnx$/, ".h");
95
+ return includeText.replace(`<${filename}.cnx>`, `<${headerPath}>`);
96
+ }
97
+ }
98
+ }
99
+
100
+ // Fallback: simple replacement (for external includes or when resolution fails)
25
101
  return includeText.replace(`<${filename}.cnx>`, `<${filename}.h>`);
26
102
  } else if (quoteMatch) {
27
103
  const filepath = quoteMatch[1];
@@ -18,6 +18,22 @@ interface ICodeGeneratorOptions {
18
18
  * Uses temporary variables instead of compound literals for rvalue pointer params.
19
19
  */
20
20
  cppMode?: boolean;
21
+ /**
22
+ * Issue #339: Relative path from source root to source file for self-include.
23
+ * When set, self-includes will use this relative path instead of just the basename.
24
+ * Example: "Display/Utils.cnx" -> #include "Display/Utils.h"
25
+ */
26
+ sourceRelativePath?: string;
27
+ /**
28
+ * Issue #349: Include directories for resolving angle-bracket .cnx includes.
29
+ * Used to search for .cnx files referenced in #include <file.cnx> directives.
30
+ */
31
+ includeDirs?: string[];
32
+ /**
33
+ * Issue #349: Input directories for calculating relative paths.
34
+ * Used to determine the correct output path prefix for headers.
35
+ */
36
+ inputs?: string[];
21
37
  }
22
38
 
23
39
  export default ICodeGeneratorOptions;
package/src/index.ts CHANGED
@@ -196,6 +196,7 @@ async function runUnifiedMode(
196
196
  verbose: boolean,
197
197
  cppRequired: boolean,
198
198
  noCache: boolean,
199
+ parseOnly: boolean,
199
200
  headerOutDir?: string,
200
201
  ): Promise<void> {
201
202
  // Issue #337: Identify which inputs are directories (for structure preservation)
@@ -283,6 +284,7 @@ async function runUnifiedMode(
283
284
  defines,
284
285
  cppRequired,
285
286
  noCache,
287
+ parseOnly,
286
288
  });
287
289
 
288
290
  // Step 5: Compile
@@ -494,6 +496,7 @@ async function main(): Promise<void> {
494
496
  let preprocess = true;
495
497
  let verbose = false;
496
498
  let noCache = false;
499
+ let parseOnly = false;
497
500
  let headerOutDir: string | undefined;
498
501
  let cleanMode = false;
499
502
 
@@ -514,6 +517,8 @@ async function main(): Promise<void> {
514
517
  preprocess = false;
515
518
  } else if (arg === "--no-cache") {
516
519
  noCache = true;
520
+ } else if (arg === "--parse") {
521
+ parseOnly = true;
517
522
  } else if (arg === "--header-out" && i + 1 < args.length) {
518
523
  headerOutDir = args[++i];
519
524
  } else if (arg === "--clean") {
@@ -526,7 +531,26 @@ async function main(): Promise<void> {
526
531
  } else {
527
532
  defines[define] = true;
528
533
  }
529
- } else if (!arg.startsWith("-")) {
534
+ } else if (arg.startsWith("-I")) {
535
+ // Common mistake: -I is GCC syntax, we use --include
536
+ // Catches both "-I path" and "-Ipath" (no space)
537
+ console.error(`Error: Unknown flag '${arg}'`);
538
+ console.error(" Did you mean: --include <dir>");
539
+ console.error("");
540
+ console.error("Example:");
541
+ console.error(" cnext src --include path/to/headers");
542
+ process.exit(1);
543
+ } else if (arg.startsWith("--")) {
544
+ // Catch unknown long flags (e.g., --foo)
545
+ console.error(`Error: Unknown flag '${arg}'`);
546
+ showHelp();
547
+ process.exit(1);
548
+ } else if (arg.startsWith("-")) {
549
+ // Catch other unknown short flags (e.g., -x)
550
+ console.error(`Error: Unknown flag '${arg}'`);
551
+ showHelp();
552
+ process.exit(1);
553
+ } else {
530
554
  inputFiles.push(arg);
531
555
  }
532
556
  }
@@ -563,6 +587,7 @@ async function main(): Promise<void> {
563
587
  verbose,
564
588
  cppRequired,
565
589
  noCache,
590
+ parseOnly,
566
591
  headerOutDir,
567
592
  );
568
593
  }
@@ -642,6 +642,9 @@ class Pipeline {
642
642
  sourcePath: file.path, // Issue #230: For self-include header generation
643
643
  generateHeaders: this.config.generateHeaders, // Issue #230: Enable self-include when headers are generated
644
644
  cppMode: this.cppDetected, // Issue #250: C++ compatible code generation
645
+ sourceRelativePath: this.getSourceRelativePath(file.path), // Issue #339: For correct self-include paths
646
+ includeDirs: this.config.includeDirs, // Issue #349: For angle-bracket include resolution
647
+ inputs: this.config.inputs, // Issue #349: For calculating relative paths
645
648
  },
646
649
  );
647
650
 
@@ -699,14 +702,14 @@ class Pipeline {
699
702
  }
700
703
 
701
704
  /**
702
- * Get output path for a file
705
+ * Get relative path from any input directory for a file.
706
+ * Returns the relative path (e.g., "Display/Utils.cnx") or null if the file
707
+ * is not under any input directory.
708
+ *
709
+ * This is the shared logic used by getSourceRelativePath, getOutputPath,
710
+ * and getHeaderOutputPath for directory structure preservation.
703
711
  */
704
- private getOutputPath(file: IDiscoveredFile): string {
705
- // Issue #211: Derive extension from cppDetected flag
706
- const ext = this.cppDetected ? ".cpp" : ".c";
707
- const outputName = basename(file.path).replace(/\.cnx$|\.cnext$/, ext);
708
-
709
- // Check if file is in any input directory (for preserving structure)
712
+ private getRelativePathFromInputs(filePath: string): string | null {
710
713
  for (const input of this.config.inputs) {
711
714
  const resolvedInput = resolve(input);
712
715
 
@@ -715,24 +718,49 @@ class Pipeline {
715
718
  continue;
716
719
  }
717
720
 
718
- const relativePath = relative(resolvedInput, file.path);
721
+ const relativePath = relative(resolvedInput, filePath);
719
722
 
720
723
  // Check if file is under this input directory
721
724
  if (relativePath && !relativePath.startsWith("..")) {
722
- // File is under this input directory - preserve structure
723
- const outputRelative = relativePath.replace(/\.cnx$|\.cnext$/, ext);
724
- const outputPath = join(this.config.outDir, outputRelative);
725
+ return relativePath;
726
+ }
727
+ }
725
728
 
726
- const outputDir = dirname(outputPath);
727
- if (!existsSync(outputDir)) {
728
- mkdirSync(outputDir, { recursive: true });
729
- }
729
+ return null;
730
+ }
731
+
732
+ /**
733
+ * Issue #339: Get relative path from input directory for self-include generation.
734
+ * Returns the relative path (e.g., "Display/Utils.cnx") or just the basename
735
+ * if the file is not in any input directory.
736
+ */
737
+ private getSourceRelativePath(filePath: string): string {
738
+ return this.getRelativePathFromInputs(filePath) ?? basename(filePath);
739
+ }
740
+
741
+ /**
742
+ * Get output path for a file
743
+ */
744
+ private getOutputPath(file: IDiscoveredFile): string {
745
+ // Issue #211: Derive extension from cppDetected flag
746
+ const ext = this.cppDetected ? ".cpp" : ".c";
747
+
748
+ const relativePath = this.getRelativePathFromInputs(file.path);
749
+ if (relativePath) {
750
+ // File is under an input directory - preserve structure
751
+ const outputRelative = relativePath.replace(/\.cnx$|\.cnext$/, ext);
752
+ const outputPath = join(this.config.outDir, outputRelative);
730
753
 
731
- return outputPath;
754
+ const outputDir = dirname(outputPath);
755
+ if (!existsSync(outputDir)) {
756
+ mkdirSync(outputDir, { recursive: true });
732
757
  }
758
+
759
+ return outputPath;
733
760
  }
734
761
 
735
762
  // Fallback: flat output in outDir
763
+ const outputName = basename(file.path).replace(/\.cnx$|\.cnext$/, ext);
736
764
  return join(this.config.outDir, outputName);
737
765
  }
738
766
 
@@ -816,37 +844,25 @@ class Pipeline {
816
844
  * Uses headerOutDir if specified, otherwise falls back to outDir
817
845
  */
818
846
  private getHeaderOutputPath(file: IDiscoveredFile): string {
819
- const headerName = basename(file.path).replace(/\.cnx$|\.cnext$/, ".h");
820
-
821
847
  // Use headerOutDir if specified, otherwise fall back to outDir
822
848
  const headerDir = this.config.headerOutDir || this.config.outDir;
823
849
 
824
- // Check if file is in any input directory (for preserving structure)
825
- for (const input of this.config.inputs) {
826
- const resolvedInput = resolve(input);
850
+ const relativePath = this.getRelativePathFromInputs(file.path);
851
+ if (relativePath) {
852
+ // File is under an input directory - preserve structure
853
+ const outputRelative = relativePath.replace(/\.cnx$|\.cnext$/, ".h");
854
+ const outputPath = join(headerDir, outputRelative);
827
855
 
828
- // Skip if input is a file (not a directory) - can't preserve structure
829
- if (existsSync(resolvedInput) && statSync(resolvedInput).isFile()) {
830
- continue;
856
+ const outputDir = dirname(outputPath);
857
+ if (!existsSync(outputDir)) {
858
+ mkdirSync(outputDir, { recursive: true });
831
859
  }
832
860
 
833
- const relativePath = relative(resolvedInput, file.path);
834
-
835
- // Check if file is under this input directory
836
- if (relativePath && !relativePath.startsWith("..")) {
837
- const outputRelative = relativePath.replace(/\.cnx$|\.cnext$/, ".h");
838
- const outputPath = join(headerDir, outputRelative);
839
-
840
- const outputDir = dirname(outputPath);
841
- if (!existsSync(outputDir)) {
842
- mkdirSync(outputDir, { recursive: true });
843
- }
844
-
845
- return outputPath;
846
- }
861
+ return outputPath;
847
862
  }
848
863
 
849
864
  // Fallback: flat output in headerDir
865
+ const headerName = basename(file.path).replace(/\.cnx$|\.cnext$/, ".h");
850
866
  return join(headerDir, headerName);
851
867
  }
852
868
 
@@ -52,6 +52,7 @@ class Project {
52
52
  generateHeaders: this.config.generateHeaders,
53
53
  cppRequired: this.config.cppRequired,
54
54
  noCache: this.config.noCache,
55
+ parseOnly: this.config.parseOnly,
55
56
  });
56
57
  }
57
58
 
@@ -37,6 +37,9 @@ interface IProjectConfig {
37
37
 
38
38
  /** Issue #183: Disable symbol caching (default: false = cache enabled) */
39
39
  noCache?: boolean;
40
+
41
+ /** Parse only mode - validate syntax without generating output */
42
+ parseOnly?: boolean;
40
43
  }
41
44
 
42
45
  export default IProjectConfig;
@@ -5,7 +5,10 @@
5
5
 
6
6
  /* eslint-disable @typescript-eslint/no-explicit-any */
7
7
 
8
- import { CPP14Parser } from "../parser/cpp/grammar/CPP14Parser";
8
+ import {
9
+ CPP14Parser,
10
+ ClassSpecifierContext,
11
+ } from "../parser/cpp/grammar/CPP14Parser";
9
12
  import ISymbol from "../types/ISymbol";
10
13
  import ESymbolKind from "../types/ESymbolKind";
11
14
  import ESourceLanguage from "../types/ESourceLanguage";
@@ -187,13 +190,28 @@ class CppSymbolCollector {
187
190
 
188
191
  const baseType = this.extractTypeFromDeclSpecSeq(declSpecSeq);
189
192
 
193
+ // Issue #342: Track anonymous class specifiers for typedef handling
194
+ let anonymousClassSpec: ClassSpecifierContext | null = null;
195
+
190
196
  // Check for class specifier
191
197
  for (const spec of declSpecSeq.declSpecifier?.() ?? []) {
192
198
  const typeSpec = spec.typeSpecifier?.();
193
199
  if (typeSpec) {
194
200
  const classSpec = typeSpec.classSpecifier?.();
195
201
  if (classSpec) {
196
- this.collectClassSpecifier(classSpec, line);
202
+ // Check if this is a named struct/class
203
+ const classHead = classSpec.classHead?.();
204
+ const classHeadName = classHead?.classHeadName?.();
205
+ const className = classHeadName?.className?.();
206
+ const identifier = className?.Identifier?.();
207
+
208
+ if (identifier?.getText()) {
209
+ // Named struct - collect normally
210
+ this.collectClassSpecifier(classSpec, line);
211
+ } else {
212
+ // Issue #342: Anonymous struct - save for typedef handling below
213
+ anonymousClassSpec = classSpec;
214
+ }
197
215
  }
198
216
 
199
217
  const enumSpec = typeSpec.enumSpecifier?.();
@@ -218,6 +236,27 @@ class CppSymbolCollector {
218
236
  ? `${this.currentNamespace}::${name}`
219
237
  : name;
220
238
 
239
+ // Issue #342: If we have an anonymous struct and this is a typedef,
240
+ // collect struct fields using the typedef name
241
+ if (anonymousClassSpec && this.symbolTable) {
242
+ const memberSpec = anonymousClassSpec.memberSpecification?.();
243
+ if (memberSpec) {
244
+ // Add the type symbol
245
+ this.symbols.push({
246
+ name: fullName,
247
+ kind: ESymbolKind.Class, // Treat typedef'd structs as classes
248
+ sourceFile: this.sourceFile,
249
+ sourceLine: line,
250
+ sourceLanguage: ESourceLanguage.Cpp,
251
+ isExported: true,
252
+ parent: this.currentNamespace,
253
+ });
254
+ // Collect members using the typedef name
255
+ this.collectClassMembers(fullName, memberSpec);
256
+ continue;
257
+ }
258
+ }
259
+
221
260
  // Issue #322: Extract parameters for function declarations
222
261
  const params = isFunction
223
262
  ? this.extractFunctionParameters(declarator)