c-next 0.2.13 → 0.2.14

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.2.13",
3
+ "version": "0.2.14",
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",
@@ -42,10 +42,8 @@ function configureYargs(args: string[], argv: string[]) {
42
42
  .scriptName("cnext")
43
43
  .usage(
44
44
  `Usage:
45
- cnext <file.cnx> Single file (outputs file.c)
45
+ cnext <file.cnx> Entry point file (follows includes)
46
46
  cnext <file.cnx> -o <output.c> Single file with explicit output
47
- cnext <files...> -o <dir> Multi-file mode
48
- cnext <dir> Directory mode (recursive)
49
47
 
50
48
  A safer C for embedded systems development.`,
51
49
  )
@@ -156,10 +154,8 @@ A safer C for embedded systems development.`,
156
154
  // Config file documentation (shown in help)
157
155
  .epilogue(
158
156
  `Examples:
159
- cnext main.cnx # Outputs main.c (same dir)
157
+ cnext src/main.cnx # Entry point (follows includes)
160
158
  cnext main.cnx -o build/main.c # Explicit output path
161
- cnext src/*.cnx -o build/ # Multiple files to directory
162
- cnext src/ # Compile all .cnx files in src/ (recursive)
163
159
 
164
160
  Target platforms: teensy41, cortex-m7, cortex-m4, cortex-m3, cortex-m0+, cortex-m0, avr
165
161
 
@@ -46,37 +46,36 @@ class PlatformIOCommand {
46
46
  // Create cnext_build.py script
47
47
  // Issue #833: Run transpilation at import time (before compilation),
48
48
  // not as a pre-action on buildprog (which runs after compilation)
49
- const buildScript = `Import("env")
49
+ const buildScript = String.raw`Import("env")
50
50
  import subprocess
51
51
  import sys
52
52
  from pathlib import Path
53
53
 
54
54
  def transpile_cnext():
55
- """Transpile all .cnx files before build"""
56
- # Find all .cnx files in src directory
57
- src_dir = Path("src")
58
- if not src_dir.exists():
55
+ """Transpile from main.cnx entry point — cnext follows includes"""
56
+ entry = Path("src/main.cnx")
57
+ if not entry.exists():
59
58
  return
60
59
 
61
- cnx_files = list(src_dir.rglob("*.cnx"))
62
- if not cnx_files:
63
- return
64
-
65
- print(f"Transpiling {len(cnx_files)} c-next files...")
66
-
67
- for cnx_file in cnx_files:
68
- try:
69
- result = subprocess.run(
70
- ["cnext", str(cnx_file)],
71
- check=True,
72
- capture_output=True,
73
- text=True
74
- )
75
- print(f" ✓ {cnx_file.name}")
76
- except subprocess.CalledProcessError as e:
77
- print(f" ✗ Error: {cnx_file.name}")
78
- print(e.stderr)
79
- sys.exit(1)
60
+ print("Transpiling from main.cnx...")
61
+
62
+ try:
63
+ result = subprocess.run(
64
+ ["cnext", str(entry)],
65
+ check=True,
66
+ capture_output=True,
67
+ text=True
68
+ )
69
+ if result.stdout:
70
+ lines = result.stdout.strip().split("\n")
71
+ for line in lines:
72
+ if line.startswith(("Compiled", "Collected", "Generated")):
73
+ print(f" {line}")
74
+ print(" ✓ Transpilation complete")
75
+ except subprocess.CalledProcessError as e:
76
+ print(f" ✗ Transpilation failed")
77
+ print(e.stderr)
78
+ sys.exit(1)
80
79
 
81
80
  # Run transpilation at import time (before compilation starts)
82
81
  transpile_cnext()
@@ -119,13 +118,12 @@ transpile_cnext()
119
118
  console.log("✓ PlatformIO integration configured!");
120
119
  console.log("");
121
120
  console.log("Next steps:");
122
- console.log(
123
- " 1. Create .cnx files in src/ (alongside your .c/.cpp files)",
124
- );
125
- console.log(" 2. Run: pio run");
121
+ console.log(" 1. Create src/main.cnx as your entry point");
122
+ console.log(" 2. Use #include to pull in other .cnx files");
123
+ console.log(" 3. Run: pio run");
126
124
  console.log("");
127
125
  console.log(
128
- "The transpiler will automatically convert .cnx .c before each build.",
126
+ "The transpiler will follow includes from main.cnx automatically.",
129
127
  );
130
128
  console.log("Commit both .cnx and generated .c files to version control.");
131
129
  }
@@ -68,6 +68,29 @@ describe("PlatformIOCommand", () => {
68
68
  expect(scriptCall?.[1]).toContain("transpile_cnext");
69
69
  });
70
70
 
71
+ it("generates script that uses entry point, not per-file loop", () => {
72
+ vi.mocked(fs.existsSync).mockReturnValue(true);
73
+ vi.mocked(fs.readFileSync).mockReturnValue("[env:esp32]\n");
74
+
75
+ PlatformIOCommand.install();
76
+
77
+ const writeCalls = vi.mocked(fs.writeFileSync).mock.calls;
78
+ const scriptCall = writeCalls.find((call) =>
79
+ (call[0] as string).includes("cnext_build.py"),
80
+ );
81
+
82
+ expect(scriptCall).toBeDefined();
83
+ const scriptContent = scriptCall?.[1] as string;
84
+
85
+ // Should use entry point approach
86
+ expect(scriptContent).toContain('Path("src/main.cnx")');
87
+ expect(scriptContent).toContain('["cnext", str(entry)]');
88
+
89
+ // Should NOT loop over individual .cnx files
90
+ expect(scriptContent).not.toContain("for cnx_file");
91
+ expect(scriptContent).not.toContain("rglob");
92
+ });
93
+
71
94
  it("generates script that runs at import time, not as buildprog pre-action (issue #833)", () => {
72
95
  // Issue #833: buildprog fires AFTER compilation, so transpile runs too late
73
96
  // The script should run transpilation at import time (before compilation)
@@ -491,7 +491,11 @@ class Transpiler {
491
491
  );
492
492
 
493
493
  // Resolve includes from source content
494
- const resolver = new IncludeResolver(searchPaths, this.fs);
494
+ const resolver = new IncludeResolver(
495
+ searchPaths,
496
+ this.fs,
497
+ this.cppDetected,
498
+ );
495
499
  const resolved = resolver.resolve(source, sourcePath);
496
500
  this.warnings.push(...resolved.warnings);
497
501
 
@@ -864,7 +868,11 @@ class Transpiler {
864
868
  );
865
869
 
866
870
  // Resolve includes
867
- const resolver = new IncludeResolver(searchPaths, this.fs);
871
+ const resolver = new IncludeResolver(
872
+ searchPaths,
873
+ this.fs,
874
+ this.cppDetected,
875
+ );
868
876
  const resolved = resolver.resolve(content, cnxFile.path);
869
877
 
870
878
  this._collectHeaders(resolved, cnextBaseNames, headerSet);
@@ -60,12 +60,21 @@ class IncludeResolver {
60
60
 
61
61
  private readonly resolvedPaths: Set<string> = new Set();
62
62
  private readonly fs: IFileSystem;
63
+ private readonly cppMode: boolean;
63
64
 
65
+ /**
66
+ * @param cppMode Controls .h vs .hpp extension for .cnx include directives.
67
+ * Note: In the Transpiler, cppDetected may change after IncludeResolver runs
68
+ * (e.g., when a .hpp header is discovered during Stage 2). HeaderGeneratorUtils
69
+ * uses stem-based dedup to handle any resulting .h/.hpp mismatch.
70
+ */
64
71
  constructor(
65
72
  private readonly searchPaths: string[],
66
73
  fs: IFileSystem = defaultFs,
74
+ cppMode: boolean = false,
67
75
  ) {
68
76
  this.fs = fs;
77
+ this.cppMode = cppMode;
69
78
  }
70
79
 
71
80
  /**
@@ -164,7 +173,8 @@ class IncludeResolver {
164
173
  // Issue #854: Track header directive for cnext includes so their types
165
174
  // can be mapped by ExternalTypeHeaderBuilder, preventing duplicate
166
175
  // forward declarations (MISRA Rule 5.6)
167
- const headerPath = includeInfo.path.replace(/\.cnx$|\.cnext$/, ".h");
176
+ const ext = this.cppMode ? ".hpp" : ".h";
177
+ const headerPath = includeInfo.path.replace(/\.cnx$|\.cnext$/, ext);
168
178
  const directive = includeInfo.isLocal
169
179
  ? `#include "${headerPath}"`
170
180
  : `#include <${headerPath}>`;
@@ -315,6 +315,56 @@ describe("IncludeResolver", () => {
315
315
  });
316
316
  });
317
317
 
318
+ // ========================================================================
319
+ // C++ Mode (cppMode) — Issue: .hpp vs .h in headerIncludeDirectives
320
+ // ========================================================================
321
+
322
+ describe("cppMode header directive extension", () => {
323
+ it("should use .h extension for cnx includes in C mode (default)", () => {
324
+ const resolver = new IncludeResolver([includeDir]);
325
+ const content = '#include "shared.cnx"';
326
+
327
+ const result = resolver.resolve(content);
328
+
329
+ const directives = [...result.headerIncludeDirectives.values()];
330
+ expect(directives).toHaveLength(1);
331
+ expect(directives[0]).toBe('#include "shared.h"');
332
+ });
333
+
334
+ it("should use .hpp extension for cnx includes in C++ mode", () => {
335
+ const resolver = new IncludeResolver([includeDir], undefined, true);
336
+ const content = '#include "shared.cnx"';
337
+
338
+ const result = resolver.resolve(content);
339
+
340
+ const directives = [...result.headerIncludeDirectives.values()];
341
+ expect(directives).toHaveLength(1);
342
+ expect(directives[0]).toBe('#include "shared.hpp"');
343
+ });
344
+
345
+ it("should use .hpp for angle-bracket cnx includes in C++ mode", () => {
346
+ const resolver = new IncludeResolver([includeDir], undefined, true);
347
+ const content = "#include <shared.cnx>";
348
+
349
+ const result = resolver.resolve(content);
350
+
351
+ const directives = [...result.headerIncludeDirectives.values()];
352
+ expect(directives).toHaveLength(1);
353
+ expect(directives[0]).toBe("#include <shared.hpp>");
354
+ });
355
+
356
+ it("should not affect C/C++ header directives (only cnx includes)", () => {
357
+ const resolver = new IncludeResolver([includeDir], undefined, true);
358
+ const content = '#include "types.h"';
359
+
360
+ const result = resolver.resolve(content);
361
+
362
+ const directives = [...result.headerIncludeDirectives.values()];
363
+ expect(directives).toHaveLength(1);
364
+ expect(directives[0]).toBe('#include "types.h"');
365
+ });
366
+ });
367
+
318
368
  // ========================================================================
319
369
  // Edge Cases
320
370
  // ========================================================================
@@ -289,22 +289,29 @@ class HeaderGeneratorUtils {
289
289
  }
290
290
 
291
291
  // External type header includes (skip duplicates of user includes)
292
- // Note: headersToInclude comes from IncludeResolver which always produces .h
293
- // (it runs before cppDetected is set). We need to match against userIncludes
294
- // which may have .hpp in C++ mode, so normalize both for deduplication.
292
+ // Dedup by basename stem to handle:
293
+ // - Different path styles (e.g., <AppConfig.hpp> vs "../AppConfig.hpp")
294
+ // - Extension mismatch from timing (.h from IncludeResolver before cppDetected,
295
+ // .hpp from IncludeExtractor after cppDetected)
295
296
  const userIncludeSet = new Set(options.userIncludes ?? []);
296
- // Issue #941: Also add .h variants for dedup since headersToInclude uses .h
297
- if (options.cppMode && options.userIncludes) {
298
- for (const inc of options.userIncludes) {
299
- // Add the .h version so we can match against headersToInclude
300
- const hVersion = inc.replace(/\.hpp"/, '.h"').replace(/\.hpp>/, ".h>");
301
- userIncludeSet.add(hVersion);
302
- }
303
- }
297
+ const extractStem = (inc: string): string => {
298
+ const match = /["<]([^">]+)[">]/.exec(inc);
299
+ if (!match) return inc;
300
+ return match[1].replace(/^.*\//, "").replace(/\.(?:h|hpp)$/, "");
301
+ };
302
+ const userIncludeStems = new Set(
303
+ (options.userIncludes ?? []).map(extractStem),
304
+ );
304
305
  for (const directive of headersToInclude) {
305
- if (!userIncludeSet.has(directive)) {
306
- lines.push(directive);
306
+ if (userIncludeSet.has(directive)) {
307
+ continue;
308
+ }
309
+ // Check if a user include already covers the same file
310
+ const stem = extractStem(directive);
311
+ if (stem && userIncludeStems.has(stem)) {
312
+ continue;
307
313
  }
314
+ lines.push(directive);
308
315
  }
309
316
 
310
317
  // Add blank line if any includes were added
@@ -571,17 +571,31 @@ describe("HeaderGeneratorUtils", () => {
571
571
  expect(lines).toContain('#include "types.hpp"');
572
572
  });
573
573
 
574
- it("deduplicates headersToInclude against userIncludes with .h/.hpp normalization in C++ mode", () => {
574
+ it("deduplicates headersToInclude against userIncludes by basename in C++ mode", () => {
575
575
  const result = HeaderGeneratorUtils.generateIncludes(
576
576
  {
577
- userIncludes: ['#include "types.hpp"'],
577
+ userIncludes: ["#include <AppConfig.hpp>"],
578
+ cppMode: true,
579
+ },
580
+ new Set(['#include "../AppConfig.hpp"']),
581
+ );
582
+ // Should NOT have duplicate - different path styles for same file should dedup
583
+ const configIncludes = result.filter((l) => l.includes("AppConfig"));
584
+ expect(configIncludes).toEqual(["#include <AppConfig.hpp>"]);
585
+ });
586
+
587
+ it("deduplicates .h vs .hpp extension mismatch from timing difference", () => {
588
+ // IncludeExtractor produces .hpp (after cppDetected), IncludeResolver
589
+ // produces .h (before cppDetected) — stem-based dedup handles this
590
+ const result = HeaderGeneratorUtils.generateIncludes(
591
+ {
592
+ userIncludes: ["#include <Display/AppData.hpp>"],
578
593
  cppMode: true,
579
594
  },
580
- new Set(['#include "types.h"']),
595
+ new Set(["#include <Display/AppData.h>"]),
581
596
  );
582
- // Should NOT have duplicate - the .h version should be filtered
583
- const typeIncludes = result.filter((l) => l.includes("types"));
584
- expect(typeIncludes).toEqual(['#include "types.hpp"']);
597
+ const appDataIncludes = result.filter((l) => l.includes("AppData"));
598
+ expect(appDataIncludes).toEqual(["#include <Display/AppData.hpp>"]);
585
599
  });
586
600
  });
587
601