@typecad/cuttlefish 1.0.0-alpha.3 → 1.0.0-alpha.7

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 (123) hide show
  1. package/README.md +4 -4
  2. package/dist/api/shared/display-adapter.d.ts +2 -1
  3. package/dist/api/shared/display-adapter.js +8 -1
  4. package/dist/api/shared/display-adapters/sdl.js +9 -2
  5. package/dist/api/shared/display-profile.d.ts +20 -3
  6. package/dist/api/shared/display-profile.js +21 -6
  7. package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
  8. package/dist/api/shared/framework-manifest-registry.js +25 -0
  9. package/dist/api/shared/framework-manifest.d.ts +462 -0
  10. package/dist/api/shared/framework-manifest.js +149 -0
  11. package/dist/api/shared/glcdfont.d.ts +12 -0
  12. package/dist/api/shared/glcdfont.js +124 -0
  13. package/dist/api/shared/graphics-strategy.d.ts +28 -0
  14. package/dist/api/shared/hal-op-ir.d.ts +427 -1
  15. package/dist/api/shared/hal-op-ir.js +95 -1
  16. package/dist/api/shared/index.d.ts +11 -1
  17. package/dist/api/shared/index.js +14 -0
  18. package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
  19. package/dist/api/shared/native-display-op-resolver.js +64 -0
  20. package/dist/api/shared/platform-strategy.d.ts +9 -0
  21. package/dist/api/shared/promise-runtime.js +78 -0
  22. package/dist/api/shared/types.d.ts +8 -0
  23. package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
  24. package/dist/api/shared/validate-framework-manifest.js +417 -0
  25. package/dist/cli-utils.d.ts +1 -0
  26. package/dist/cli-utils.js +3 -1
  27. package/dist/cli.js +175 -4
  28. package/dist/config-loader.js +20 -1
  29. package/dist/config-schema.d.ts +36 -36
  30. package/dist/create/board-spec.d.ts +4 -4
  31. package/dist/create/index.d.ts +1 -1
  32. package/dist/create/index.js +1 -1
  33. package/dist/create/init-scaffold.d.ts +3 -0
  34. package/dist/create/init-scaffold.js +74 -2
  35. package/dist/create/init-templates.d.ts +4 -0
  36. package/dist/create/init-templates.js +217 -17
  37. package/dist/create/init-wizard.js +20 -1
  38. package/dist/emit/cpp-emitter.js +4 -3
  39. package/dist/emit/emitters/emitter-context.d.ts +5 -0
  40. package/dist/emit/emitters/function-emitter-impl.js +69 -59
  41. package/dist/emit/emitters/output-finalizer.d.ts +6 -0
  42. package/dist/emit/emitters/output-finalizer.js +21 -11
  43. package/dist/emit/emitters/setup.js +155 -0
  44. package/dist/emit/emitters/top-level-prep.js +2 -0
  45. package/dist/emit/emitters/ui-emitter.js +23 -8
  46. package/dist/emit/expression-renderer.js +10 -1
  47. package/dist/emit/snprintf-helpers.js +8 -0
  48. package/dist/emit/statement-renderer.js +13 -0
  49. package/dist/emit/utils/async-state-machine.js +185 -116
  50. package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
  51. package/dist/emit/utils/hal-op-cpp-type.js +40 -0
  52. package/dist/ir/adc-range-validation.js +26 -25
  53. package/dist/ir/build-ir.js +5 -1
  54. package/dist/ir/expression-to-ir.js +57 -6
  55. package/dist/ir/feature-registry.js +7 -25
  56. package/dist/ir/hal/hal-emitter.d.ts +5 -2
  57. package/dist/ir/hal/hal-emitter.js +40 -12
  58. package/dist/ir/hal/hal-parser.d.ts +6 -0
  59. package/dist/ir/hal/hal-parser.js +74 -0
  60. package/dist/ir/hal/hal-plugins.js +571 -0
  61. package/dist/ir/identifier-collector.js +18 -0
  62. package/dist/ir/interrupt-analysis.js +8 -3
  63. package/dist/ir/memory-budget-validation.js +1 -0
  64. package/dist/ir/network-validation.d.ts +4 -0
  65. package/dist/ir/network-validation.js +184 -0
  66. package/dist/ir/ownership-analysis.js +33 -1
  67. package/dist/ir/peripheral-ownership.js +5 -0
  68. package/dist/ir/peripheral-validation.d.ts +1 -1
  69. package/dist/ir/peripheral-validation.js +6 -3
  70. package/dist/ir/pin-alias-conflict.d.ts +1 -1
  71. package/dist/ir/pin-alias-conflict.js +2 -1
  72. package/dist/ir/pin-capability-validation.js +34 -32
  73. package/dist/ir/pin-mode-validation.js +5 -0
  74. package/dist/ir/pin-safety.d.ts +1 -1
  75. package/dist/ir/pin-safety.js +2 -1
  76. package/dist/ir/program-analysis.d.ts +39 -0
  77. package/dist/ir/program-analysis.js +192 -0
  78. package/dist/ir/pulldown-validation.d.ts +1 -1
  79. package/dist/ir/pulldown-validation.js +2 -1
  80. package/dist/ir/pwm-timer-sharing.d.ts +1 -1
  81. package/dist/ir/pwm-timer-sharing.js +2 -1
  82. package/dist/ir/resource-analysis.js +2 -0
  83. package/dist/ir/timer0-pwm-timing-conflict.d.ts +1 -1
  84. package/dist/ir/timer0-pwm-timing-conflict.js +2 -1
  85. package/dist/ir/timing-validation.d.ts +6 -1
  86. package/dist/ir/timing-validation.js +51 -12
  87. package/dist/ir/transformers/expressions.js +58 -0
  88. package/dist/ir/transformers/hal-emit-helpers.js +1 -1
  89. package/dist/ir/transformers/variables.js +86 -19
  90. package/dist/ir/try-catch-validation.js +2 -0
  91. package/dist/ir/type-resolution.js +2 -2
  92. package/dist/ir/unit-suspicion-validation.js +9 -7
  93. package/dist/ir/validation-orchestrator.js +9 -7
  94. package/dist/libdef/c-to-decl.d.ts +27 -0
  95. package/dist/libdef/c-to-decl.js +397 -0
  96. package/dist/libdef/component-decls.d.ts +2 -0
  97. package/dist/libdef/component-decls.js +6 -0
  98. package/dist/libdef/component-discovery.d.ts +43 -0
  99. package/dist/libdef/component-discovery.js +83 -0
  100. package/dist/libdef/cpp-to-decl.d.ts +9 -0
  101. package/dist/libdef/cpp-to-decl.js +72 -0
  102. package/dist/libdef/idf-discovery.d.ts +7 -0
  103. package/dist/libdef/idf-discovery.js +59 -0
  104. package/dist/libdef/registry.js +5 -2
  105. package/dist/licenses.d.ts +185 -0
  106. package/dist/licenses.js +963 -0
  107. package/dist/lint-cache.d.ts +59 -0
  108. package/dist/lint-cache.js +257 -0
  109. package/dist/orchestrator/graph-builder.js +6 -2
  110. package/dist/stores/display-profile-store.d.ts +1 -0
  111. package/dist/stores/display-profile-store.js +1 -0
  112. package/dist/testing.d.ts +4 -2
  113. package/dist/testing.js +4 -2
  114. package/dist/transpile.d.ts +3 -0
  115. package/dist/transpile.js +78 -32
  116. package/dist/types.d.ts +5 -1
  117. package/dist/ui-hook.d.ts +17 -1
  118. package/dist/utils/cli.js +71 -1
  119. package/dist/utils/fs.d.ts +13 -0
  120. package/dist/utils/fs.js +50 -0
  121. package/package.json +9 -4
  122. package/dist/ir/heap-array-validation.d.ts +0 -24
  123. package/dist/ir/heap-array-validation.js +0 -29
@@ -13,12 +13,16 @@ export interface InitProjectOptions {
13
13
  baudRate?: number;
14
14
  includeSketch: boolean;
15
15
  toolchainType?: string;
16
+ /** Extra frameworkData fields (e.g. `{ target: 'esp32s3' }` for framework-esp32). */
17
+ frameworkData?: Record<string, unknown>;
16
18
  }
17
19
  export declare function generateProjectPackageJson(options: InitProjectOptions): string;
18
20
  export declare function generateProjectTsconfig(options: InitProjectOptions): string;
19
21
  export declare function generateProjectConfig(options: InitProjectOptions): string;
20
22
  export declare function generateProjectEnvDts(options: InitProjectOptions): string;
21
23
  export declare function generateStarterSketch(options: InitProjectOptions): string;
24
+ export declare function generateStarterTest(_options: InitProjectOptions): string;
25
+ export declare function generateStarterSim(options: InitProjectOptions): string;
22
26
  export declare function generateBoardForwardingFile(boardPackage: string): string;
23
27
  export declare function generateGitignore(_options: InitProjectOptions): string;
24
28
  export declare function generateEslintConfig(_options: InitProjectOptions): string;
@@ -2,11 +2,11 @@ import { LINT_RULES } from '../ir/feature-registry.js';
2
2
  export function generateProjectPackageJson(options) {
3
3
  const { projectName, frameworkPackage, boardPackage } = options;
4
4
  const deps = {
5
- "@typecad/cuttlefish": "^0.1.0-alpha.1",
6
- [frameworkPackage]: "^0.1.0-alpha.1",
5
+ "@typecad/cuttlefish": "^1.0.0-alpha.3",
6
+ [frameworkPackage]: "^1.0.0-alpha.3",
7
7
  };
8
8
  if (boardPackage) {
9
- deps[boardPackage] = "^0.1.0-alpha.1";
9
+ deps[boardPackage] = "^1.0.0-alpha.3";
10
10
  }
11
11
  const depsJson = Object.entries(deps)
12
12
  .map(([k, v]) => ` "${k}": "${v}"`)
@@ -15,12 +15,27 @@ export function generateProjectPackageJson(options) {
15
15
  // eslint-transpiler-rules plugin (which is plain JS, no dep). Without these
16
16
  // devDependencies `npm run lint` fails to resolve the parser/plugin in a
17
17
  // freshly created project. Versions mirror the repo demo's package.json.
18
- const devDepsJson = [
18
+ const baseDevDeps = [
19
19
  ' "eslint": "^10.4.1"',
20
20
  ' "@typescript-eslint/parser": "^8.61.0"',
21
21
  ' "@typescript-eslint/eslint-plugin": "^8.61.0"',
22
- ].join(',\n');
22
+ ];
23
+ // Developer-utility scripts shared by every target (native + embedded).
24
+ // None require hardware or extra dependencies:
25
+ // dev — auto-retranspile on save (transpile-only; append --compile
26
+ // to also compile on each change). The fast "does it
27
+ // typecheck" feedback loop.
28
+ // gen-decls — generate TypeScript .d.ts from C++ headers. File-argument:
29
+ // `npm run gen-decls -- lib/foo.h` or `-- --all lib/`.
30
+ // gen-libdefs — generate library-definition stubs from a TS file's imports.
31
+ // File-argument: `npm run gen-libdefs -- src/main.ts`.
32
+ const devScripts = [
33
+ '"dev": "cuttlefish build --watch"',
34
+ '"gen-decls": "cuttlefish gen-decls"',
35
+ '"gen-libdefs": "cuttlefish gen-libdefs"',
36
+ ];
23
37
  if (options.isNative) {
38
+ const devDepsJson = baseDevDeps.join(',\n');
24
39
  return `{
25
40
  "name": "${projectName}",
26
41
  "version": "1.0.0",
@@ -28,7 +43,8 @@ export function generateProjectPackageJson(options) {
28
43
  "scripts": {
29
44
  "build": "cuttlefish build",
30
45
  "compile": "cuttlefish build --compile",
31
- "lint": "eslint --config .cuttlefish/eslint.config.mjs src/"
46
+ "lint": "eslint --config .cuttlefish/eslint.config.mjs src/",
47
+ ${devScripts.join(',\n ')}
32
48
  },
33
49
  "dependencies": {
34
50
  ${depsJson}
@@ -39,6 +55,19 @@ ${devDepsJson}
39
55
  }
40
56
  `;
41
57
  }
58
+ // Embedded projects get two host-side testing tiers:
59
+ // - @typecad/expect: hardware tests run on the board via cuttlefish-test
60
+ // (`npm run test:hw`), scoped to tests/**/*.test.ts.
61
+ // - @typecad/simulator + vitest: simulate the board in Node (`npm run
62
+ // simulate`), scoped to sim/**/*.test.ts so vitest never collides with the
63
+ // @typecad/expect no-op stubs under tests/.
64
+ // Versions mirror the workspace's published releases / root devDeps.
65
+ const devDepsJson = [
66
+ ...baseDevDeps,
67
+ ' "@typecad/expect": "^1.0.0-alpha.3"',
68
+ ' "@typecad/simulator": "^1.0.0-alpha.3"',
69
+ ' "vitest": "^4.0.18"',
70
+ ].join(',\n');
42
71
  return `{
43
72
  "name": "${projectName}",
44
73
  "version": "1.0.0",
@@ -48,7 +77,10 @@ ${devDepsJson}
48
77
  "compile": "cuttlefish build --compile",
49
78
  "upload": "cuttlefish build --compile --upload",
50
79
  "monitor": "cuttlefish build --compile --upload --monitor",
51
- "lint": "eslint --config .cuttlefish/eslint.config.mjs src/"
80
+ "test:hw": "npm exec -- cuttlefish-test",
81
+ "simulate": "vitest run sim/",
82
+ "lint": "eslint --config .cuttlefish/eslint.config.mjs src/",
83
+ ${devScripts.join(',\n ')}
52
84
  },
53
85
  "dependencies": {
54
86
  ${depsJson}
@@ -85,9 +117,10 @@ export function generateProjectTsconfig(options) {
85
117
  "noEmit": true,
86
118
  "resolveJsonModule": true,
87
119
  "allowArbitraryExtensions": true,
120
+ "allowImportingTsExtensions": true,
88
121
  "rootDirs": ["src", "types"]${paths}
89
122
  },
90
- "include": ["src/**/*.ts", "types/**/*.ts", "cuttlefish.config.ts"${options.boardPackage ? ', ".cuttlefish/cuttlefish-env.d.ts"' : ''}]
123
+ "include": ["src/**/*.ts", "types/**/*.ts", "cuttlefish.config.ts"${options.boardPackage ? ', ".cuttlefish/cuttlefish-env.d.ts"' : ''}${options.isNative ? '' : ', "sim/**/*.ts"'}]
91
124
  }
92
125
  `;
93
126
  }
@@ -117,8 +150,26 @@ export default config;
117
150
  `;
118
151
  }
119
152
  const buildTarget = options.buildTarget;
120
- const resolvedToolchain = options.toolchainType ?? 'arduino-cli';
121
- const buildTargetLine = buildTarget ? `\n // Framework data\n frameworkData: {\n buildTarget: '${buildTarget}',\n },` : '';
153
+ const isEspIdf = options.frameworkPackage === '@typecad/framework-esp32'
154
+ || options.framework === 'esp32'
155
+ || options.toolchainType === 'idf';
156
+ const resolvedToolchain = options.toolchainType
157
+ ?? (isEspIdf ? 'idf' : 'arduino-cli');
158
+ // framework-esp32 wants IDF chip ids in frameworkData (target + buildTarget),
159
+ // not Arduino FQBNs. Known IDF targets already set frameworkData.target;
160
+ // wizard/CLI may still pass an FQBN in buildTarget — normalize to the last segment.
161
+ let frameworkDataBlock = '';
162
+ if (isEspIdf) {
163
+ const idfTarget = options.frameworkData?.target
164
+ ?? options.frameworkData?.buildTarget
165
+ ?? (buildTarget?.includes(':') ? buildTarget.split(':').filter(Boolean).pop() : buildTarget)
166
+ ?? options.architecture
167
+ ?? 'esp32';
168
+ frameworkDataBlock = `\n // Framework data (IDF chip target)\n frameworkData: {\n target: '${idfTarget}',\n buildTarget: '${idfTarget}',\n },`;
169
+ }
170
+ else if (buildTarget) {
171
+ frameworkDataBlock = `\n // Framework data\n frameworkData: {\n buildTarget: '${buildTarget}',\n },`;
172
+ }
122
173
  const mcuLine = options.mcu
123
174
  ? `\n // MCU package — provides silicon-level pin definitions\n mcu: '${options.mcu.startsWith('@') ? options.mcu : `@typecad/mcu-${options.mcu}`}',\n`
124
175
  : '';
@@ -127,6 +178,11 @@ export default config;
127
178
  : '';
128
179
  const portHint = process.platform === 'win32' ? 'COM4' : '/dev/ttyACM0';
129
180
  const baudLine = options.baudRate ? `\n\n // Console polyfill configuration\n console: {\n baudRate: ${options.baudRate},\n // Serial port for upload/monitor. Override with --port on the CLI.\n port: '${portHint}',\n },` : '';
181
+ // Hardware test runner configuration — used by \`npm run test:hw\` (cuttlefish-test,
182
+ // provided by @typecad/expect). It transpiles each tests/**/*.test.ts file,
183
+ // flashes it to the board, and evaluates the assertions over serial.
184
+ const resolvedBaud = options.baudRate ?? 115200;
185
+ const testLine = `\n\n // Hardware test runner (@typecad/expect / \`npm run test:hw\`)\n test: {\n // Serial port for the test board. Override with --port on the CLI or the\n // CUTTLEFISH_PORT env var (e.g. CUTTLEFISH_PORT=/dev/ttyUSB0 npm run test:hw).\n port: '${portHint}',\n baudRate: ${resolvedBaud},\n timeout: 30000,\n include: ['tests/**/*.test.ts'],\n },`;
130
186
  return `// ---------------------------------------------------------------------------
131
187
  // cuttlefish.config.ts — Project configuration
132
188
  //
@@ -143,7 +199,7 @@ const config: CuttlefishConfig = {
143
199
  target: '${options.architecture}',${mcuLine}${boardLine}
144
200
 
145
201
  // Framework package — controls code generation strategy
146
- framework: '${options.frameworkPackage}',${buildTargetLine}
202
+ framework: '${options.frameworkPackage}',${frameworkDataBlock}
147
203
 
148
204
  // Output / build options
149
205
  output: {
@@ -155,7 +211,7 @@ const config: CuttlefishConfig = {
155
211
  // Toolchain configuration
156
212
  toolchain: {
157
213
  type: '${resolvedToolchain}',
158
- },${baudLine}
214
+ },${baudLine}${testLine}
159
215
  };
160
216
 
161
217
  export default config;
@@ -286,6 +342,148 @@ while (true) {
286
342
  }
287
343
  `;
288
344
  }
345
+ export function generateStarterTest(_options) {
346
+ return `// ---------------------------------------------------------------------------
347
+ // Hardware test — Basics
348
+ //
349
+ // Runs on the board via \`npm run test:hw\` (cuttlefish-test). Each test file is
350
+ // transpiled, flashed to the board, and its assertions are evaluated on the host
351
+ // over serial. Change the serial port in cuttlefish.config.ts (the \`test.port\`
352
+ // field) or override it with the CUTTLEFISH_PORT env var.
353
+ //
354
+ // API: describe(...).it(...).expect(value).<matcher>() chains. Import pin
355
+ // objects from '@typecad/board' to assert on real hardware I/O. Every file ends
356
+ // with done().
357
+ // ---------------------------------------------------------------------------
358
+
359
+ import { describe, done } from '@typecad/expect';
360
+
361
+ describe("Basics")
362
+ .it("adds two numbers")
363
+ .expect(
364
+ (() => {
365
+ const a = 1;
366
+ let b = 2;
367
+ return a + b;
368
+ })
369
+ ).toBe(3)
370
+ .it("multiplies two numbers")
371
+ .expect(
372
+ (() => {
373
+ let a = 3;
374
+ let b = 4;
375
+ return a * b;
376
+ })
377
+ ).toBe(12)
378
+ .it("reads an array element")
379
+ .expect(
380
+ (() => {
381
+ const data = new Uint8Array([0xAA, 0x10, 0x20]);
382
+ return data[1];
383
+ })
384
+ ).toBe(0x10)
385
+ .it("clamps a value to a range")
386
+ .expect(
387
+ (() => {
388
+ const value = 2000;
389
+ return Math.max(0, Math.min(1023, value));
390
+ })
391
+ ).toBe(1023);
392
+
393
+ done();
394
+ `;
395
+ }
396
+ export function generateStarterSim(options) {
397
+ const boardType = options.targetId;
398
+ return `// ---------------------------------------------------------------------------
399
+ // Hardware simulation — Button + LED
400
+ //
401
+ // Runs entirely on your computer with \`npm run simulate\` (vitest + the
402
+ // @typecad/simulator package). No board, serial port, or arduino-cli required.
403
+ // The simulator mirrors the pins/peripherals of your ${options.targetDisplayName}
404
+ // (${boardType}); you inject fake inputs and assert on the outputs in Node.
405
+ //
406
+ // This is the fast tier — iterate on logic here, then confirm on real hardware
407
+ // with \`npm run test:hw\` (which flashes tests/ to the board).
408
+ // ---------------------------------------------------------------------------
409
+
410
+ import { describe, it, expect, beforeEach } from "vitest";
411
+ import {
412
+ createSimBoard,
413
+ type SimBoard,
414
+ type SimDigitalPin,
415
+ } from "@typecad/simulator";
416
+
417
+ // ===========================================================================
418
+ // FIRMWARE LOGIC
419
+ // ---------------------------------------------------------------------------
420
+ // Factor your firmware into a function that takes the simulated pins as
421
+ // arguments. In a real project this same logic runs on the board against real
422
+ // pins — here it runs against the sim board so you can test it without hardware.
423
+ // ===========================================================================
424
+
425
+ /**
426
+ * Reads a button and reflects its state on an LED.
427
+ *
428
+ * To use your own logic: replace the body of this function with whatever your
429
+ * firmware does (read a sensor, drive a motor, print to serial, ...). As long
430
+ * as it only touches pins you pass in, the simulator can exercise it.
431
+ */
432
+ function reflectButtonOnLed(button: SimDigitalPin, led: SimDigitalPin): void {
433
+ // The button pin is pulled HIGH (1) at rest and reads LOW (0) when pressed.
434
+ if (button.isLow()) {
435
+ led.high();
436
+ } else {
437
+ led.low();
438
+ }
439
+ }
440
+
441
+ // ===========================================================================
442
+ // TEST BENCH
443
+ // ---------------------------------------------------------------------------
444
+ // \`createSimBoard\` builds an in-memory version of your board. The pin numbers
445
+ // below match the physical pinout. Add the pins/peripherals your firmware uses:
446
+ // board.digital(n), board.analog(n), board.pwm(n), board.serial(n),
447
+ // board.i2c(n), board.spi(n), board.interrupt(n).
448
+ // ===========================================================================
449
+
450
+ function setupSim(): { board: SimBoard; button: SimDigitalPin; led: SimDigitalPin } {
451
+ // boardType mirrors the target chosen with \`cuttlefish create\`.
452
+ const board = createSimBoard({ boardType: "${boardType}" });
453
+
454
+ const button = board.digital(2).asInputPullUp(); // button on pin 2 (INPUT_PULLUP)
455
+ const led = board.digital(13).asOutput(false); // LED on pin 13
456
+
457
+ return { board, button, led };
458
+ }
459
+
460
+ describe("Button + LED (simulator)", () => {
461
+ beforeEach(() => {
462
+ // A fresh board per test keeps state isolated. For a long-running sim you
463
+ // can call board.reset() between cycles instead.
464
+ });
465
+
466
+ it("keeps the LED off while the button is released", () => {
467
+ const { button, led } = setupSim();
468
+
469
+ // Button at rest: INPUT_PULLUP reads HIGH.
470
+ reflectButtonOnLed(button, led);
471
+
472
+ expect(led.getBitValue()).toBe(0);
473
+ });
474
+
475
+ it("turns the LED on while the button is pressed", () => {
476
+ const { button, led } = setupSim();
477
+
478
+ // Simulate a press: drive the button pin LOW.
479
+ button.injectValue(0);
480
+ reflectButtonOnLed(button, led);
481
+
482
+ expect(led.getBitValue()).toBe(1);
483
+ });
484
+ });
485
+ `;
486
+ }
289
487
  export function generateBoardForwardingFile(boardPackage) {
290
488
  return `// ---------------------------------------------------------------------------
291
489
  // .cuttlefish/board.ts — Dynamically generated forwarding board package
@@ -328,11 +526,13 @@ export default [
328
526
  files: ["src/**/*.ts"],
329
527
  languageOptions: {
330
528
  parser: tsparser,
331
- parserOptions: {
332
- // Type-aware parsing so @typescript-eslint/no-explicit-any resolves
333
- // imported bindings to their real types instead of defaulting to any.
334
- project: "./tsconfig.json",
335
- },
529
+ // NOTE: do NOT set parserOptions.project here. None of the rules below
530
+ // (no-restricted-syntax, the cuttlefish/* AST rules, no-explicit-any,
531
+ // no-eval, ...) consume type information, so enabling type-aware linting
532
+ // only forces ESLint to build a full TS type-program per file — ~3.4s of
533
+ // pure overhead on small projects with zero change to what is detected.
534
+ // If a future rule needs types, scope project to that rule only via
535
+ // parserOptions on a dedicated config block, not globally.
336
536
  },
337
537
  plugins: {
338
538
  "@typescript-eslint": tseslint,
@@ -94,6 +94,11 @@ export async function runInitWizard(partialOptions) {
94
94
  if (target.architecture === 'avr') {
95
95
  frameworkOptions.push({ label: "Bare-metal AVR (PORTB, etc.)", value: 'avr', pkg: '@typecad/framework-avr' });
96
96
  }
97
+ // ESP32 family: offer the native ESP-IDF flavor alongside Arduino.
98
+ if (target.architecture === 'esp32' || target.architecture === 'esp32s3'
99
+ || target.architecture === 'esp32c3' || target.architecture === 'esp32c6') {
100
+ frameworkOptions.push({ label: "ESP32 (native ESP-IDF)", value: 'esp32', pkg: '@typecad/framework-esp32' });
101
+ }
97
102
  if (partialOptions?.framework) {
98
103
  const match = frameworkOptions.find(f => f.value === partialOptions.framework);
99
104
  framework = match?.value ?? partialOptions.framework;
@@ -141,6 +146,10 @@ export async function runInitWizard(partialOptions) {
141
146
  else {
142
147
  includeSketch = await promptConfirm(rl, "Create starter sketch?", true);
143
148
  }
149
+ const isEspIdf = frameworkPackage === '@typecad/framework-esp32' || framework === 'esp32';
150
+ const idfTarget = target.frameworkData?.target
151
+ ?? target.architecture
152
+ ?? 'esp32';
144
153
  return {
145
154
  projectName,
146
155
  targetId: target.id,
@@ -150,10 +159,20 @@ export async function runInitWizard(partialOptions) {
150
159
  boardPackage: target.boardPackage,
151
160
  frameworkPackage,
152
161
  framework,
153
- buildTarget: target.buildTarget,
162
+ buildTarget: isEspIdf ? idfTarget : target.buildTarget,
154
163
  mcu: target.mcu,
155
164
  baudRate,
156
165
  includeSketch,
166
+ ...(isEspIdf
167
+ ? {
168
+ toolchainType: 'idf',
169
+ frameworkData: target.frameworkData?.target
170
+ ? { ...target.frameworkData }
171
+ : { target: idfTarget, buildTarget: idfTarget },
172
+ }
173
+ : target.frameworkData
174
+ ? { frameworkData: target.frameworkData }
175
+ : {}),
157
176
  };
158
177
  }
159
178
  catch (err) {
@@ -1,5 +1,5 @@
1
1
  import { buildEmitterContext } from "./emitters/setup.js";
2
- import { emitPreamble } from "./emitters/output-finalizer.js";
2
+ import { emitPreamble, emitAsyncTaskClasses, finalizeOutput } from "./emitters/output-finalizer.js";
3
3
  import { runTopLevelPreprocessing } from "./emitters/top-level-prep.js";
4
4
  import { synthesizeEntrypoints } from "./emitters/entrypoint-synthesizer.js";
5
5
  import { emitTypeDeclarations } from "./emitters/type-decl-emitter.js";
@@ -7,7 +7,6 @@ import { emitNamespaces } from "./emitters/namespace-emitter.js";
7
7
  import { emitClasses } from "./emitters/class-emitter.js";
8
8
  import { emitPostClassDeclarations, emitCallbackFunctions, emitFunctions, emitFunctionForwardDeclarations } from "./emitters/function-emitter-impl.js";
9
9
  import { emitUIRuntime } from "./emitters/ui-emitter.js";
10
- import { finalizeOutput } from "./emitters/output-finalizer.js";
11
10
  const globalEnumNames = new Set();
12
11
  const globalLargeEnumNames = new Set();
13
12
  export function registerAllEnumNames(enums) {
@@ -21,7 +20,7 @@ export function registerAllEnumNames(enums) {
21
20
  export function emitCpp(program, options) {
22
21
  // 1. Build shared emitter context (strategy, renderers, analysis, etc.)
23
22
  const ctx = buildEmitterContext(program, options);
24
- // 2. Emit preamble (includes, polyfills, async task classes, shims)
23
+ // 2. Emit preamble (includes, polyfills, shims) — async task classes deferred
25
24
  emitPreamble(ctx);
26
25
  // 2.5. Emit UI runtime (header structs + static tables) — entry file only,
27
26
  // when a UI is mounted. File-scope, must precede any function using it.
@@ -32,6 +31,8 @@ export function emitCpp(program, options) {
32
31
  synthesizeEntrypoints(ctx);
33
32
  // 5. Emit type declarations (enums, type aliases, interfaces, top-level constants)
34
33
  emitTypeDeclarations(ctx);
34
+ // 5.5. Async state machines — after globals so WIFI_SSID etc. are in scope
35
+ emitAsyncTaskClasses(ctx);
35
36
  // 6. Emit namespaces
36
37
  emitNamespaces(ctx);
37
38
  // 6.5. Emit function forward declarations (split mode — must precede class definitions)
@@ -56,6 +56,11 @@ export interface CallbackFunction {
56
56
  params: string[];
57
57
  statements: StatementIR[];
58
58
  debounceMs?: number;
59
+ /**
60
+ * True for GPIO / hardware ISR callbacks only. WiFi event handlers, timers,
61
+ * and setInterval use the same hoist path but must NOT get IRAM_ATTR.
62
+ */
63
+ isInterruptHandler?: boolean;
59
64
  /**
60
65
  * Return type of the synthesized free function. Defaults to "void" (the
61
66
  * historical ISR/HAL-callback case). Populated from a hoisted lambda's
@@ -128,7 +128,12 @@ export function emitCallbackFunctions(ctx) {
128
128
  appendSourceLine(ctx, `const unsigned long ${callback.name}_debounce = ${callback.debounceMs};`);
129
129
  appendSourceLine(ctx, "");
130
130
  }
131
- appendSourceLine(ctx, `${strategy.isrFunctionAttribute?.() ?? ""}${renderCallbackSignature(callback)} {`);
131
+ // IRAM_ATTR only on true ISR definitions. ESP-IDF's IRAM_ATTR uses
132
+ // __COUNTER__, so putting it on both forward decl and definition assigns
133
+ // conflicting .iram1.N sections (-Werror=attributes). Non-ISR callbacks
134
+ // (WiFi events, timers) must not be IRAM-placed either — they call printf.
135
+ const isrAttr = callback.isInterruptHandler ? (strategy.isrFunctionAttribute?.() ?? "") : "";
136
+ appendSourceLine(ctx, `${isrAttr}${renderCallbackSignature(callback)} {`);
132
137
  if (callback.debounceMs !== undefined && callback.debounceMs > 0) {
133
138
  appendSourceLine(ctx, ` volatile unsigned long now = ${strategy.currentTimeMillis()};`);
134
139
  appendSourceLine(ctx, ` if (now - ${callback.name}_lastTime < ${callback.name}_debounce) return;`);
@@ -141,10 +146,11 @@ export function emitCallbackFunctions(ctx) {
141
146
  appendSourceLine(ctx, "}");
142
147
  appendSourceLine(ctx, "");
143
148
  }
144
- // Split-mode ISR callback forward declarations in header
149
+ // Split-mode ISR callback forward declarations in header (no IRAM_ATTR —
150
+ // attribute belongs on the definition only; see comment above).
145
151
  if (effectiveEmitMode === "split") {
146
152
  for (const callback of ctx.callbackFunctions) {
147
- appendHeaderLine(ctx, `${strategy.isrFunctionAttribute?.() ?? ""}${renderCallbackSignature(callback)};`);
153
+ appendHeaderLine(ctx, `${renderCallbackSignature(callback)};`);
148
154
  }
149
155
  }
150
156
  }
@@ -172,6 +178,10 @@ export function emitFunctions(ctx) {
172
178
  // exported functions (which ride along with their definition below).
173
179
  for (let fi = 0; fi < mappedFunctions.length; fi++) {
174
180
  const fn = mappedFunctions[fi];
181
+ // Async functions become cooperative *Task state machines (emitAsyncTaskClasses).
182
+ // Emitting an empty stub here triggers -Wunused-function with no value.
183
+ if (fn.isAsync && ctx.hasAsyncRuntime)
184
+ continue;
175
185
  const declarationParameterList = ctx.statementRenderer.renderParameters(fn.parameters, true);
176
186
  const definitionParameterList = ctx.statementRenderer.renderParameters(fn.parameters, false);
177
187
  const readonlyPrefix = fn.isReadonlyReturnType ? "const " : "";
@@ -246,65 +256,60 @@ export function emitFunctions(ctx) {
246
256
  if (hasPromiseRuntime && fn.name === asyncDriverFn) {
247
257
  appendSourceLine(ctx, " cuttlefish_pump_microtasks();");
248
258
  }
249
- if (fn.isAsync && ctx.hasAsyncRuntime) {
250
- appendSourceLine(ctx, ` // driven as cooperative task in ${asyncDriverFn}()`);
251
- }
252
- else {
253
- if (fn.typeParameterConstraints) {
254
- for (const [param, expr] of fn.typeParameterConstraints) {
255
- appendSourceLine(ctx, ` static_assert(${expr}, "${param} constraint violated");`);
256
- }
259
+ if (fn.typeParameterConstraints) {
260
+ for (const [param, expr] of fn.typeParameterConstraints) {
261
+ appendSourceLine(ctx, ` static_assert(${expr}, "${param} constraint violated");`);
257
262
  }
258
- const functionScope = createChildEmissionScope(topLevelScope, fn.parameters);
259
- ctx.cArrayVarNames = ctx.fnCArrayVarNames.get(String(fi)) ?? new Set();
260
- const isLoopDriver = fn.name === asyncDriverFn;
261
- const lastStmt = fn.statements.length > 0 ? fn.statements[fn.statements.length - 1] : null;
262
- const lastIsReturn = lastStmt?.kind === "return";
263
- const stmtsToEmit = (isLoopDriver && lastIsReturn)
264
- ? fn.statements.slice(0, -1)
265
- : fn.statements;
266
- for (const statement of stmtsToEmit) {
267
- appendRenderedStatement(ctx, statement, " ", functionScope);
268
- }
269
- // Drive the UI runtime each frame. Fires only in the driver function when
270
- // a UI is mounted (entryHasUI). Uses a separate gate from the async pump
271
- // so a pure-UI program (no async/timers) still animates. When the strategy
272
- // provides hostEventLoop, the per-frame work runs inside a while loop so a
273
- // host target (SDL) can pump events and present between ticks. Emitted
274
- // AFTER the function's init statements (ui_init/display_init/touch_init)
275
- // so initialization precedes the loop.
276
- if (entryHasUI() && fn.name === asyncDriverFn) {
277
- const loop = strategy.hostEventLoop?.();
278
- if (loop) {
279
- appendSourceLine(ctx, ` bool ${loop.flagName} = true;`);
280
- appendSourceLine(ctx, ` while (${loop.continueCondition}) {`);
281
- appendSourceLine(ctx, ` ${loop.preIteration}`);
282
- }
283
- appendSourceLine(ctx, ` uint32_t __tc_ui_now = (uint32_t)${strategy.currentTimeMillis()};`);
284
- appendSourceLine(ctx, " static uint32_t __tc_ui_last_tick = __tc_ui_now;");
285
- appendSourceLine(ctx, " uint32_t __tc_ui_delta = __tc_ui_now - __tc_ui_last_tick;");
286
- appendSourceLine(ctx, " __tc_ui_last_tick = __tc_ui_now;");
287
- appendSourceLine(ctx, " if (__tc_ui_delta > 250) __tc_ui_delta = 250;");
288
- appendSourceLine(ctx, " ui_tick((uint16_t)__tc_ui_delta);");
289
- if (loop) {
290
- appendSourceLine(ctx, ` ${loop.postIteration}`);
291
- appendSourceLine(ctx, ` }`);
292
- }
263
+ }
264
+ const functionScope = createChildEmissionScope(topLevelScope, fn.parameters);
265
+ ctx.cArrayVarNames = ctx.fnCArrayVarNames.get(String(fi)) ?? new Set();
266
+ const isLoopDriver = fn.name === asyncDriverFn;
267
+ const lastStmt = fn.statements.length > 0 ? fn.statements[fn.statements.length - 1] : null;
268
+ const lastIsReturn = lastStmt?.kind === "return";
269
+ const stmtsToEmit = (isLoopDriver && lastIsReturn)
270
+ ? fn.statements.slice(0, -1)
271
+ : fn.statements;
272
+ for (const statement of stmtsToEmit) {
273
+ appendRenderedStatement(ctx, statement, " ", functionScope);
274
+ }
275
+ // Drive the UI runtime each frame. Fires only in the driver function when
276
+ // a UI is mounted (entryHasUI). Uses a separate gate from the async pump
277
+ // so a pure-UI program (no async/timers) still animates. When the strategy
278
+ // provides hostEventLoop, the per-frame work runs inside a while loop so a
279
+ // host target (SDL) can pump events and present between ticks. Emitted
280
+ // AFTER the function's init statements (ui_init/display_init/touch_init)
281
+ // so initialization precedes the loop.
282
+ if (entryHasUI() && fn.name === asyncDriverFn) {
283
+ const loop = strategy.hostEventLoop?.();
284
+ if (loop) {
285
+ appendSourceLine(ctx, ` bool ${loop.flagName} = true;`);
286
+ appendSourceLine(ctx, ` while (${loop.continueCondition}) {`);
287
+ appendSourceLine(ctx, ` ${loop.preIteration}`);
293
288
  }
294
- if (isLoopDriver && (hasPromiseRuntime || asyncTaskClasses.length > 0 || usesTimers)) {
295
- const taskNames = asyncTaskClasses.map(t => t.taskVarName);
296
- const asyncConfig = strategy.getAsyncRuntimeConfig();
297
- asyncConfig.hasPromiseRuntime = hasPromiseRuntime;
298
- asyncConfig.hasTimers = usesTimers;
299
- const injectionLines = strategy.asyncLoopInjection(taskNames, asyncConfig);
300
- for (const line of injectionLines) {
301
- appendSourceLine(ctx, ` ${line}`);
302
- }
289
+ appendSourceLine(ctx, ` uint32_t __tc_ui_now = (uint32_t)${strategy.currentTimeMillis()};`);
290
+ appendSourceLine(ctx, " static uint32_t __tc_ui_last_tick = __tc_ui_now;");
291
+ appendSourceLine(ctx, " uint32_t __tc_ui_delta = __tc_ui_now - __tc_ui_last_tick;");
292
+ appendSourceLine(ctx, " __tc_ui_last_tick = __tc_ui_now;");
293
+ appendSourceLine(ctx, " if (__tc_ui_delta > 250) __tc_ui_delta = 250;");
294
+ appendSourceLine(ctx, " ui_tick((uint16_t)__tc_ui_delta);");
295
+ if (loop) {
296
+ appendSourceLine(ctx, ` ${loop.postIteration}`);
297
+ appendSourceLine(ctx, ` }`);
303
298
  }
304
- if (isLoopDriver && lastIsReturn) {
305
- appendRenderedStatement(ctx, lastStmt, " ", functionScope);
299
+ }
300
+ if (isLoopDriver && (hasPromiseRuntime || asyncTaskClasses.length > 0 || usesTimers)) {
301
+ const taskNames = asyncTaskClasses.map(t => t.taskVarName);
302
+ const asyncConfig = strategy.getAsyncRuntimeConfig();
303
+ asyncConfig.hasPromiseRuntime = hasPromiseRuntime;
304
+ asyncConfig.hasTimers = usesTimers;
305
+ const injectionLines = strategy.asyncLoopInjection(taskNames, asyncConfig);
306
+ for (const line of injectionLines) {
307
+ appendSourceLine(ctx, ` ${line}`);
306
308
  }
307
309
  }
310
+ if (isLoopDriver && lastIsReturn) {
311
+ appendRenderedStatement(ctx, lastStmt, " ", functionScope);
312
+ }
308
313
  appendSourceLine(ctx, "}");
309
314
  emitCommentLines(fn.trailingComments, "", (line) => appendSourceLine(ctx, line));
310
315
  appendSourceLine(ctx, "");
@@ -338,16 +343,21 @@ export function emitFunctions(ctx) {
338
343
  export function emitFunctionForwardDeclarations(ctx) {
339
344
  const { strategy, effectiveEmitMode } = ctx;
340
345
  const excludedNames = new Set(strategy.forwardDeclarationExclusions?.() ?? []);
341
- // ISR callback forward declarations.
346
+ // Callback forward declarations (no IRAM_ATTR — ESP-IDF's attribute uses
347
+ // __COUNTER__, so decl+def would get conflicting .iram1.N sections).
342
348
  if (effectiveEmitMode !== "split") {
343
349
  for (const callback of ctx.callbackFunctions) {
344
- appendSourceLine(ctx, `${strategy.isrFunctionAttribute?.() ?? ""}${renderCallbackSignature(callback)};`);
350
+ appendSourceLine(ctx, `${renderCallbackSignature(callback)};`);
345
351
  }
346
352
  }
347
353
  let emittedAnyFn = false;
348
354
  for (const fn of ctx.mappedFunctions) {
349
355
  if (excludedNames.has(fn.name))
350
356
  continue;
357
+ // Async functions become *Task classes; calls lower to `fooTask.run()`.
358
+ // Forward-declaring empty stubs triggers -Wunused-function.
359
+ if (fn.isAsync && ctx.hasAsyncRuntime)
360
+ continue;
351
361
  const isExported = fn.isExported === true;
352
362
  // A function is an entrypoint if it is the strategy's primary entrypoint
353
363
  // (e.g. `setup` on Arduino, `main` on native) OR one of the platform's
@@ -1,4 +1,10 @@
1
1
  import type { GeneratedOutputs } from "../../types.js";
2
2
  import type { EmitterContext } from "./emitter-context.js";
3
3
  export declare function emitPreamble(ctx: EmitterContext): void;
4
+ /**
5
+ * Emit cooperative async state-machine classes. Must run AFTER top-level
6
+ * globals (emitTypeDeclarations) so identifiers like WIFI_SSID referenced
7
+ * from task bodies are already declared.
8
+ */
9
+ export declare function emitAsyncTaskClasses(ctx: EmitterContext): void;
4
10
  export declare function finalizeOutput(ctx: EmitterContext): GeneratedOutputs;