@typecad/cuttlefish 1.0.0-alpha.10 → 1.0.0-alpha.12

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.
@@ -5,4 +5,4 @@ import type { RuntimePolyfillIR } from "../api/shared/index.js";
5
5
  * Build a RuntimePolyfillIR for the async Promise runtime, if the program
6
6
  * has async functions and the target architecture has stdlib support.
7
7
  */
8
- export declare function buildAsyncRuntimePolyfill(program: ProgramIR, ctx: PlatformContext | undefined, target: string, queueCapacity?: number): RuntimePolyfillIR | null;
8
+ export declare function buildAsyncRuntimePolyfill(program: ProgramIR, ctx: PlatformContext | undefined, target: string, queueCapacity?: number, strategy?: import("../api/shared/platform-strategy.js").PlatformStrategy): RuntimePolyfillIR | null;
@@ -3,7 +3,7 @@ import { getStdLibSupport, generatePromiseRuntime } from "../api/shared/index.js
3
3
  * Build a RuntimePolyfillIR for the async Promise runtime, if the program
4
4
  * has async functions and the target architecture has stdlib support.
5
5
  */
6
- export function buildAsyncRuntimePolyfill(program, ctx, target, queueCapacity) {
6
+ export function buildAsyncRuntimePolyfill(program, ctx, target, queueCapacity, strategy) {
7
7
  const hasAsync = program.functions.some(fn => fn.isAsync);
8
8
  if (!hasAsync)
9
9
  return null;
@@ -11,13 +11,22 @@ export function buildAsyncRuntimePolyfill(program, ctx, target, queueCapacity) {
11
11
  const stdlib = getStdLibSupport(architecture);
12
12
  if (!stdlib.hasVector || !stdlib.hasString)
13
13
  return null;
14
+ // Pass the strategy through so the runtime's now-expression matches the
15
+ // target (generic bakes a std::chrono expression via currentTimeMillis()
16
+ // instead of a millis() token the generic target never defines). When the
17
+ // expression uses std::chrono, the polyfill must carry <chrono> itself —
18
+ // the generic strategy's forcedIncludes are empty by design.
19
+ const now = strategy?.currentTimeMillis?.() ?? "millis()";
20
+ const requiredIncludes = ["<functional>", "<vector>", "<utility>", "<string>"];
21
+ if (now.includes("std::chrono"))
22
+ requiredIncludes.push("<chrono>");
14
23
  return {
15
24
  kind: "polyfill",
16
25
  id: "async_runtime",
17
26
  domain: "standard",
18
- requiredIncludes: ["<functional>", "<vector>", "<utility>", "<string>"],
27
+ requiredIncludes,
19
28
  forwardDeclarations: [],
20
- helperStructs: [generatePromiseRuntime(queueCapacity ?? 256)],
29
+ helperStructs: [generatePromiseRuntime(queueCapacity ?? 256, false, strategy)],
21
30
  helperFunctions: [],
22
31
  shimMacros: [],
23
32
  dependencies: [],
@@ -184,7 +184,7 @@ export class GenericStrategy {
184
184
  }
185
185
  generateNativePolyfills(program, ctx) {
186
186
  const helpers = [];
187
- const asyncRuntime = buildAsyncRuntimePolyfill(program, ctx, "generic", this.asyncQueueCapacity());
187
+ const asyncRuntime = buildAsyncRuntimePolyfill(program, ctx, "generic", this.asyncQueueCapacity(), this);
188
188
  if (asyncRuntime)
189
189
  helpers.push(asyncRuntime);
190
190
  return helpers;
package/dist/transpile.js CHANGED
@@ -29,7 +29,7 @@ import { loadSafetyEngine } from "./safety/safety-bridge.js";
29
29
  import { setDisplayProfile, resetDisplayProfile } from "./stores/display-profile-store.js";
30
30
  import { setThemeCss, resetThemeCss, setThemeClass } from "./stores/theme-store.js";
31
31
  import { emitCpp, registerAllEnumNames } from "./emit/cpp-emitter.js";
32
- import { readText, writeText } from "./utils/fs.js";
32
+ import { readText, writeText, resetWrittenFiles, wasWrittenThisRun } from "./utils/fs.js";
33
33
  import { debug as logDebug, info } from "./utils/logger.js";
34
34
  import { loadLibraryDefinitions, generateLibdefStubs } from "./libdef/registry.js";
35
35
  import { buildCallGraph } from "./ir/call-graph.js";
@@ -66,12 +66,53 @@ function loadExpectPreprocessor() {
66
66
  }
67
67
  }
68
68
  function cleanOutput(_entryDir, outDir) {
69
- // Preserved for incremental-build support: writeText now skips writing when
70
- // content is identical, so keeping the existing output dir intact lets
71
- // downstream build tools (idf.py/ninja, arduino-cli) reuse their build
72
- // caches. Stale files from removed source modules are harmless — they're
73
- // not referenced by the current entry file and won't be compiled.
74
- // The output dir is still created (via writeText ensureDir) on first run.
69
+ // The out dir is NOT wiped: writeText skips writing when content is
70
+ // identical, so keeping it lets downstream build tools (idf.py/ninja,
71
+ // arduino-cli) reuse their build caches. Stale generated SOURCES are handled
72
+ // precisely instead after emission, sweepStaleGeneratedSources() removes
73
+ // compiled-source files in the out dir that this run did not write (e.g. a
74
+ // main.cpp left behind by the old emit naming next to the current src.cpp;
75
+ // Zephyr's CMakeLists globs src/*.cpp, so a leftover compiles into
76
+ // duplicate-symbol link errors). The output dir is still created (via
77
+ // writeText → ensureDir) on first run.
78
+ void outDir;
79
+ resetWrittenFiles();
80
+ }
81
+ const GENERATED_SOURCE_EXTENSIONS = [".cpp", ".cc", ".c", ".h", ".ino"];
82
+ /**
83
+ * Remove stale generated source files from the out dir: compiled-source files
84
+ * that THIS transpile run did not write. Sweeps only the source layouts the
85
+ * emit pipeline uses (out dir root, src/, main/) and never recurses — build
86
+ * trees (e.g. Zephyr's out/build with its own generated .c files) are
87
+ * untouched, and neither are sidecar JSONs.
88
+ */
89
+ function sweepStaleGeneratedSources(outDir) {
90
+ for (const sub of ["", "src", "main"]) {
91
+ const dir = sub ? path.join(outDir, sub) : outDir;
92
+ let entries;
93
+ try {
94
+ entries = fs.readdirSync(dir, { withFileTypes: true });
95
+ }
96
+ catch {
97
+ continue; // layout subdir not used by this framework
98
+ }
99
+ for (const entry of entries) {
100
+ if (!entry.isFile())
101
+ continue;
102
+ if (!GENERATED_SOURCE_EXTENSIONS.some((ext) => entry.name.toLowerCase().endsWith(ext)))
103
+ continue;
104
+ const full = path.join(dir, entry.name);
105
+ if (wasWrittenThisRun(full))
106
+ continue;
107
+ try {
108
+ fs.unlinkSync(full);
109
+ info(`Removed stale generated source: ${path.relative(process.cwd(), full)}`);
110
+ }
111
+ catch {
112
+ // Locked/read-only file — leave it; best-effort cleanup.
113
+ }
114
+ }
115
+ }
75
116
  }
76
117
  /**
77
118
  * Auto-generates .d.ts files for C++ modules that are missing declarations.
@@ -822,6 +863,13 @@ export async function transpileFile(options) {
822
863
  }
823
864
  }
824
865
  profiler.endTimer("post:flatten");
866
+ // Remove stale generated sources (renamed entries, removed modules) so
867
+ // downstream globs (Zephyr's CMakeLists src/*.cpp) don't compile leftovers
868
+ // into duplicate-symbol link errors. Runs after every write of this run,
869
+ // including the toolchain prepare hook above.
870
+ profiler.startTimer("post:sweep-stale");
871
+ sweepStaleGeneratedSources(outDir);
872
+ profiler.endTimer("post:sweep-stale");
825
873
  // Profiler session ends (profiling disabled - no report generation)
826
874
  // ── Generate diagnostics report if enabled ──────────────────────────────
827
875
  let diagnosticsReportPath;
package/dist/utils/cli.js CHANGED
@@ -409,8 +409,9 @@ export function parseCommandLine(argv) {
409
409
  platformContext: {},
410
410
  };
411
411
  }
412
- // licenses subcommand — scan installed Arduino libraries for SPDX licenses
413
- if (firstArg === "licenses") {
412
+ // licenses subcommand — scan the project's dependencies for SPDX licenses.
413
+ // Accept `license` (singular) as an alias so both spellings work.
414
+ if (firstArg === "licenses" || firstArg === "license") {
414
415
  return {
415
416
  command: "licenses",
416
417
  strict: argv.includes("--strict"),
@@ -1,5 +1,7 @@
1
1
  export declare function ensureDir(dirPath: string): void;
2
2
  export declare function readText(filePath: string): string;
3
+ export declare function resetWrittenFiles(): void;
4
+ export declare function wasWrittenThisRun(filePath: string): boolean;
3
5
  export declare function writeText(filePath: string, content: string): void;
4
6
  export declare function listFiles(dirPath: string, extension: string): string[];
5
7
  /**
package/dist/utils/fs.js CHANGED
@@ -8,10 +8,26 @@ export function ensureDir(dirPath) {
8
8
  export function readText(filePath) {
9
9
  return fs.readFileSync(filePath, "utf8");
10
10
  }
11
+ // Paths written (or confirmed identical) via writeText since the last
12
+ // resetWrittenFiles() call. The transpiler uses this to sweep stale generated
13
+ // sources from the out dir — files left behind by renamed entries or removed
14
+ // source modules that a downstream glob (e.g. Zephyr's CMakeLists
15
+ // `file(GLOB src/*.cpp)`) would otherwise compile, producing duplicate-symbol
16
+ // link errors.
17
+ const writtenFiles = new Set();
18
+ export function resetWrittenFiles() {
19
+ writtenFiles.clear();
20
+ }
21
+ export function wasWrittenThisRun(filePath) {
22
+ return writtenFiles.has(path.resolve(filePath));
23
+ }
11
24
  export function writeText(filePath, content) {
12
25
  ensureDir(path.dirname(filePath));
26
+ const resolved = path.resolve(filePath);
13
27
  // Skip writing when content is identical — preserves mtime so downstream
14
28
  // build tools (idf.py/ninja, arduino-cli, make) can skip recompilation.
29
+ // The file still counts as "written this run" (it is current output).
30
+ writtenFiles.add(resolved);
15
31
  try {
16
32
  if (fs.readFileSync(filePath, "utf8") === content)
17
33
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typecad/cuttlefish",
3
- "version": "1.0.0-alpha.10",
3
+ "version": "1.0.0-alpha.12",
4
4
  "description": "TypeScript to C++ transpiler — native, Arduino, and bare-metal targets",
5
5
  "type": "module",
6
6
  "main": "./dist/transpile.js",
@@ -99,8 +99,8 @@
99
99
  "zod": "^3.24.0"
100
100
  },
101
101
  "peerDependencies": {
102
- "@typecad/ui": "1.0.0-alpha.10",
103
- "@typecad/safety": "1.0.0-alpha.10"
102
+ "@typecad/ui": "1.0.0-alpha.12",
103
+ "@typecad/safety": "1.0.0-alpha.12"
104
104
  },
105
105
  "peerDependenciesMeta": {
106
106
  "@typecad/ui": {
@@ -111,7 +111,8 @@
111
111
  }
112
112
  },
113
113
  "optionalDependencies": {
114
- "@typecad/framework-native": "1.0.0-alpha.10"
114
+ "@typecad/expect": "1.0.0-alpha.12",
115
+ "@typecad/framework-native": "1.0.0-alpha.12"
115
116
  },
116
117
  "devDependencies": {
117
118
  "@types/node": "^22.10.7"