@typecad/cuttlefish 1.0.0-alpha.12 → 1.0.0-alpha.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.
Files changed (101) hide show
  1. package/assets/editor-extensions/typecad-debug/README.md +142 -0
  2. package/assets/editor-extensions/typecad-debug/out/extension.js +347 -0
  3. package/assets/editor-extensions/typecad-debug/out/extension.js.map +1 -0
  4. package/assets/editor-extensions/typecad-debug/package.json +76 -0
  5. package/assets/editor-extensions/typecad-ui/LICENSE +27 -0
  6. package/assets/editor-extensions/typecad-ui/README.md +52 -0
  7. package/assets/editor-extensions/typecad-ui/file-icon-dark.png +0 -0
  8. package/assets/editor-extensions/typecad-ui/file-icon-light.png +0 -0
  9. package/assets/editor-extensions/typecad-ui/icon.png +0 -0
  10. package/assets/editor-extensions/typecad-ui/language-configuration.json +43 -0
  11. package/assets/editor-extensions/typecad-ui/package.json +54 -0
  12. package/assets/editor-extensions/typecad-ui/snippets/typecad-ui.json +80 -0
  13. package/assets/editor-extensions/typecad-ui/syntaxes/markdown-ui.json +45 -0
  14. package/assets/editor-extensions/typecad-ui/syntaxes/typecad-ui.tmLanguage.json +1269 -0
  15. package/dist/add-preset.d.ts +4 -0
  16. package/dist/add-preset.js +74 -0
  17. package/dist/api/config.d.ts +8 -5
  18. package/dist/api/shared/display-adapters/sdl.js +1 -1
  19. package/dist/api/shared/display-profile.d.ts +11 -0
  20. package/dist/api/shared/display-profile.js +3 -0
  21. package/dist/api/shared/framework-manifest.d.ts +61 -504
  22. package/dist/api/shared/hal-op-ir.d.ts +19 -0
  23. package/dist/api/shared/toolchain-types.d.ts +0 -1
  24. package/dist/cli.js +31 -9
  25. package/dist/config-loader.d.ts +0 -2
  26. package/dist/config-loader.js +13 -6
  27. package/dist/config-schema.d.ts +17 -230
  28. package/dist/config-schema.js +1 -3
  29. package/dist/contract/contract-parser.d.ts +9 -127
  30. package/dist/create/board-checklist.js +1 -1
  31. package/dist/create/board-codegen.js +4 -4
  32. package/dist/create/board-spec.d.ts +74 -530
  33. package/dist/create/debug-artifacts.d.ts +20 -0
  34. package/dist/create/debug-artifacts.js +69 -0
  35. package/dist/create/editor-integration.d.ts +30 -0
  36. package/dist/create/editor-integration.js +218 -0
  37. package/dist/create/eslint-rules-template.d.ts +2 -2
  38. package/dist/create/framework-catalog.d.ts +8 -0
  39. package/dist/create/framework-catalog.js +28 -5
  40. package/dist/create/index.d.ts +9 -6
  41. package/dist/create/index.js +6 -4
  42. package/dist/create/{init-scaffold.d.ts → scaffold.d.ts} +5 -4
  43. package/dist/create/{init-scaffold.js → scaffold.js} +38 -2
  44. package/dist/create/templates.d.ts +29 -0
  45. package/dist/create/{init-templates.js → templates.js} +15 -2
  46. package/dist/create/wizard.d.ts +8 -0
  47. package/dist/create/{init-wizard.js → wizard.js} +11 -4
  48. package/dist/emit/compliance/rules.js +18 -4
  49. package/dist/emit/emitters/function-emitter-impl.js +7 -1
  50. package/dist/emit/emitters/line-appender.js +6 -0
  51. package/dist/emit/emitters/setup.js +22 -0
  52. package/dist/emit/emitters/ui-emitter.js +40 -15
  53. package/dist/emit/route-hal-op.js +55 -1
  54. package/dist/emit/statement-renderer.js +5 -2
  55. package/dist/ir/build-ir.js +46 -4
  56. package/dist/ir/expression-to-ir.js +17 -0
  57. package/dist/ir/hal/hal-emitter.js +23 -5
  58. package/dist/ir/hal/hal-plugins.js +11 -0
  59. package/dist/ir/pin-mode-validation.js +32 -9
  60. package/dist/ir/pin-state-tracking.d.ts +58 -0
  61. package/dist/ir/pin-state-tracking.js +182 -0
  62. package/dist/ir/program-analysis.d.ts +6 -0
  63. package/dist/ir/program-analysis.js +38 -0
  64. package/dist/ir/statement-to-ir.js +14 -0
  65. package/dist/ir/transformers/control-flow.js +29 -0
  66. package/dist/ir/transformers/ui-call-resolver.js +105 -1
  67. package/dist/ir/ui-element-auto-wire.js +7 -4
  68. package/dist/library/catalog.d.ts +35 -0
  69. package/dist/library/catalog.js +67 -0
  70. package/dist/library/cli.d.ts +2 -0
  71. package/dist/library/cli.js +168 -0
  72. package/dist/library/init.d.ts +25 -0
  73. package/dist/library/init.js +397 -0
  74. package/dist/library/install.d.ts +9 -0
  75. package/dist/library/install.js +80 -0
  76. package/dist/library/registry-search.d.ts +34 -0
  77. package/dist/library/registry-search.js +50 -0
  78. package/dist/library/validate.d.ts +17 -0
  79. package/dist/library/validate.js +175 -0
  80. package/dist/library-packages.d.ts +95 -0
  81. package/dist/library-packages.js +275 -0
  82. package/dist/orchestrator/graph-builder.d.ts +4 -1
  83. package/dist/orchestrator/graph-builder.js +23 -1
  84. package/dist/preview/api-shared-shim.d.ts +1 -0
  85. package/dist/preview/api-shared-shim.js +7 -0
  86. package/dist/preview/client.js +235 -2
  87. package/dist/preview/server.js +154 -62
  88. package/dist/testing.d.ts +2 -2
  89. package/dist/testing.js +1 -1
  90. package/dist/theme-tokens.d.ts +22 -0
  91. package/dist/theme-tokens.js +172 -0
  92. package/dist/transpile.js +90 -5
  93. package/dist/types.d.ts +24 -0
  94. package/dist/ui-hook.d.ts +7 -0
  95. package/dist/utils/cli.d.ts +2 -2
  96. package/dist/utils/cli.js +77 -3
  97. package/dist/utils/ui.d.ts +5 -0
  98. package/dist/utils/ui.js +7 -0
  99. package/package.json +18 -9
  100. package/dist/create/init-templates.d.ts +0 -28
  101. package/dist/create/init-wizard.d.ts +0 -8
@@ -1,8 +1,8 @@
1
1
  import * as readline from "node:readline/promises";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
3
  import chalk from "chalk";
4
- import { KNOWN_TARGETS } from "./init-scaffold.js";
5
- import { frameworksForTarget, frameworkCatalogEntry, FRAMEWORK_CATALOG, frameworkTargetProfile } from "./framework-catalog.js";
4
+ import { KNOWN_TARGETS } from "./scaffold.js";
5
+ import { frameworksForTarget, frameworkCatalogEntry, frameworkCompatibleWithTarget, FRAMEWORK_CATALOG, frameworkTargetProfile } from "./framework-catalog.js";
6
6
  function validateProjectName(name) {
7
7
  if (!name || name.trim().length === 0) {
8
8
  return "Project name cannot be empty.";
@@ -53,7 +53,7 @@ async function promptConfirm(rl, prompt, defaultValue) {
53
53
  return defaultValue;
54
54
  return trimmed === "y" || trimmed === "yes";
55
55
  }
56
- export async function runInitWizard(partialOptions) {
56
+ export async function runCreateWizard(partialOptions) {
57
57
  const rl = readline.createInterface({ input, output });
58
58
  try {
59
59
  console.log();
@@ -101,9 +101,16 @@ export async function runInitWizard(partialOptions) {
101
101
  if (partialOptions?.framework) {
102
102
  const requested = frameworkCatalogEntry(partialOptions.framework);
103
103
  if (!requested) {
104
- const available = FRAMEWORK_CATALOG.filter((f) => f.installable).map((f) => f.id).join(", ");
104
+ const available = FRAMEWORK_CATALOG.filter(f => f.installable).map(f => f.id).join(", ");
105
105
  throw new Error(`Unknown framework '${partialOptions.framework}'. Available: ${available}`);
106
106
  }
107
+ // Same compatibility guard as the non-interactive CLI path: a preseeded
108
+ // --framework bypasses the narrowing below, so validate it explicitly.
109
+ if (!frameworkCompatibleWithTarget(target, requested.id)) {
110
+ const list = compatible.map(f => f.id).join(", ");
111
+ throw new Error(`Framework '${requested.id}' is not compatible with target '${target.id}' (${target.displayName}). ` +
112
+ `Compatible frameworks: ${list}`);
113
+ }
107
114
  framework = requested.id;
108
115
  frameworkPackage = requested.packageName;
109
116
  console.log(`${chalk.cyan("?")} Framework: ${chalk.white(requested.label)}`);
@@ -146,8 +146,8 @@ export const RULES = [
146
146
  },
147
147
  ],
148
148
  },
149
- { id: "A18-5-10", title: "No malloc/calloc/realloc", severity: "required", category: "C",
150
- detect: /\b(malloc|calloc|realloc)\s*\(/, enabled: true,
149
+ { id: "A18-5-10", title: "No malloc/calloc/realloc/free (C dynamic memory family)", severity: "required", category: "C",
150
+ detect: /\b(?:ps_)?(?:malloc|calloc|realloc|free)\s*\(/, enabled: true,
151
151
  knownPatterns: [
152
152
  {
153
153
  // Offscreen canvas allocation (CuttlefishCanvas16/CuttlefishCanvasMono
@@ -163,10 +163,24 @@ export const RULES = [
163
163
  // the alternative (operator new) is the very thing that crashes. The
164
164
  // object is placement-constructed on the malloc'd memory and freed via
165
165
  // an explicit dtor + free, so the vtable/lifetime are correct.
166
- detect: /malloc\s*\(/,
167
- justification: "Canvas object/buffer allocation on full-libcpp-without-exceptions targets; malloc avoids the operator-new std::bad_alloc → std::terminate → abort path. OOM returns NULL and the runtime degrades gracefully.",
166
+ // ps_malloc is the ESP32 PSRAM variant of the same constraint.
167
+ detect: /(?:ps_)?malloc\s*\(/,
168
+ justification: "Canvas object/buffer allocation on full-libcpp-without-exceptions targets; malloc (or ESP32 ps_malloc) avoids the operator-new std::bad_alloc → std::terminate → abort path. OOM returns NULL and the runtime degrades gracefully.",
168
169
  kind: "ts-literal",
169
170
  },
171
+ {
172
+ // Offscreen canvas teardown, the release side of the allocations
173
+ // above (canvas objects and their malloc'd/ps_malloc'd pixel
174
+ // buffers). On Arduino cores operator new is malloc-backed and
175
+ // free() releases both SRAM and PSRAM objects via the ESP32 unified
176
+ // heap, so dtor + free() is the correct teardown for every canvas
177
+ // allocation path — `delete` would be UB on the placement-new PSRAM
178
+ // object. Only these named canvas/buffer releases are deviations;
179
+ // any other free() stays an unrecorded violation.
180
+ detect: /\bfree\s*\(\s*(?:canvas|buffer_|psramBuf)\s*\)/,
181
+ justification: "Canvas teardown on targets whose operator new is malloc-backed (Arduino cores, ESP32 unified heap): the object was placement-constructed or allocation-path-compatible, so dtor + free() is the only well-defined release; delete would be UB on placement-new PSRAM objects.",
182
+ kind: "raw-array",
183
+ },
170
184
  ],
171
185
  },
172
186
  { id: "A27-0-4", title: "No function returning std::move of local", severity: "required", category: "C",
@@ -31,7 +31,13 @@ export function emitPostClassDeclarations(ctx) {
31
31
  // and failed at g++ time. Demo #28 Finding B.
32
32
  if (ctx.promotedVarDecls.size > 0) {
33
33
  for (const [varName, info] of ctx.promotedVarDecls) {
34
- appendSourceLine(ctx, `${info.cppType} ${escapeCppKeyword(varName, platformReservedNames)} = {};`);
34
+ // A3-9-1: promoted file-scope declarations bypass renderVarDecl, so the
35
+ // int -> fixed-width substitution has to be applied here as well.
36
+ const autosarOn = ctx.compliance.isEnabled() && ctx.compliance.isBanned("A3-9-1");
37
+ const fwdType = autosarOn && info.cppType === "int"
38
+ ? strategy.defaultNumericType(ctx.compliance)
39
+ : normalizeCppTypeForTarget(info.cppType);
40
+ appendSourceLine(ctx, `${fwdType} ${escapeCppKeyword(varName, platformReservedNames)} = {};`);
35
41
  // Seed the top-level scope's type map so subsequent assign rendering
36
42
  // (e.g. the deferred `c = SafeInt(0)` initializer) can resolve the
37
43
  // variable's type and inject template args / casts via
@@ -298,6 +298,12 @@ export function appendRenderedStatement(ctx, statement, indent, scopeState) {
298
298
  return;
299
299
  }
300
300
  if (statement.kind === "block") {
301
+ if (statement.body.length === 0) {
302
+ // An empty block is a no-op statement — skip it entirely so lowered
303
+ // top-level statements don't litter setup() with bare { } pairs.
304
+ emitCommentLines(statement.trailingComments, indent, (line) => appendSourceLine(ctx, line));
305
+ return;
306
+ }
301
307
  appendSourceLine(ctx, `${indent}{`, { tsSpan: statement.sourceSpan, nodeKind: statement.kind });
302
308
  const nestedScope = cloneEmissionScopeState(scopeState);
303
309
  for (const nested of statement.body)
@@ -5,6 +5,7 @@ import { analyzeProgram } from "../../ir/program-analysis.js";
5
5
  import { collectStatementIdentifiers } from "../../ir/identifier-collector.js";
6
6
  import { ensureDir } from "../../utils/fs.js";
7
7
  import { resolveImport } from "../../libdef/registry.js";
8
+ import { isRegisteredCuttlefishLibrary } from "../../library-packages.js";
8
9
  import { emitPolyfillBoilerplate } from "../native-helpers-emitter.js";
9
10
  import { resolveStrategy } from "../../platform/registry.js";
10
11
  import { buildCoopSchedulerPolyfill } from "../../platform/coop-scheduler-runtime.js";
@@ -547,6 +548,14 @@ export function buildEmitterContext(program, options) {
547
548
  if (!programAnalysis.usesPreferences) {
548
549
  shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_PREFERENCES_BEGIN', '// CUTTLEFISH_PREFERENCES_END');
549
550
  }
551
+ // Entry-TU marker for framework shims: the UI runtime header (ui_tick) is
552
+ // emitted ONLY into the entry file (emitUIRuntime gates on isEntryFile), so
553
+ // shim helpers that keep the UI alive during bounded blocking waits (e.g.
554
+ // Zephyr's wifi connect) must compile those calls down everywhere else.
555
+ // Defined before the shim lines land in the preamble, after all filters.
556
+ if (isEntryFile && entryHasUI() && shimLines.length > 0) {
557
+ shimLines.unshift("#define CUTTLEFISH_ENTRY_UI_TU 1");
558
+ }
550
559
  profileDiagnostics = [...strategy.profileDiagnostics(program, options.platformContext)];
551
560
  }
552
561
  for (const imported of program.imports) {
@@ -554,6 +563,19 @@ export function buildEmitterContext(program, options) {
554
563
  // named imports — add it to the symbolMap so cross-module references
555
564
  // resolve. Demo #12 Finding C — was skipped, so `encode` wasn't declared.
556
565
  const defaultName = imported.defaultImportName;
566
+ // Cuttlefish library packages: the import emits the shim-header include
567
+ // (resolved through the libdef registry from the library manifest) and
568
+ // contributes native shims + build fragments. This must run BEFORE the
569
+ // SDK skip below — @typecad-scoped libraries would otherwise be treated
570
+ // as type-level-only like the rest of the SDK.
571
+ if (isRegisteredCuttlefishLibrary(imported.moduleSpecifier)) {
572
+ const resolved = resolveImport(imported, options.libdefs, options.target, options.platformContext, program.fileName);
573
+ includes.push(normalizeInclude(resolved.include));
574
+ Object.assign(symbolMap, resolved.symbolMap);
575
+ if (defaultName)
576
+ symbolMap[defaultName] = defaultName;
577
+ continue;
578
+ }
557
579
  if (isCuttlefishSDKImport(imported.moduleSpecifier, program.fileName)) {
558
580
  for (const symbol of imported.namedImports) {
559
581
  symbolMap[symbol] = symbol;
@@ -46,8 +46,13 @@ export function generateTouchPollBody(input) {
46
46
  const hasNativeSize = profile.nativeWidth !== undefined && profile.nativeHeight !== undefined;
47
47
  const nativeWidth = profile.nativeWidth ?? profile.width;
48
48
  const nativeHeight = profile.nativeHeight ?? profile.height;
49
- const rawMapX = `map(__rawX, ${xMin}, ${xMax}, 0, ${nativeWidth})`;
50
- const rawMapY = `map(__rawY, ${yMin}, ${yMax}, 0, ${nativeHeight})`;
49
+ // Inlined Arduino map() arithmetic, NOT a map() call: the emitted body must
50
+ // be self-contained. The map/constrain helpers are capability-gated
51
+ // (usesMap is set by SCRIPT-level map() calls only), so a bare map() here
52
+ // failed to link on frameworks without a core map() (Zephyr).
53
+ const mapCall = (raw, inMin, inMax, outMin, outMax) => `((${raw} - ${inMin}) * (${outMax} - ${outMin}) / (${inMax} - ${inMin}) + ${outMin})`;
54
+ const rawMapX = mapCall("__rawX", xMin, xMax, 0, nativeWidth);
55
+ const rawMapY = mapCall("__rawY", yMin, yMax, 0, nativeHeight);
51
56
  let mapX, mapY;
52
57
  let sdlClamp = false;
53
58
  if (library === "sdl") {
@@ -69,15 +74,15 @@ export function generateTouchPollBody(input) {
69
74
  const invertY = rotation === 1 || rotation === 2;
70
75
  if (isLandscape) {
71
76
  mapX = invertX
72
- ? `map(__rawX, ${xMin}, ${xMax}, ${profile.width}, 0)`
73
- : `map(__rawX, ${xMin}, ${xMax}, 0, ${profile.width})`;
77
+ ? mapCall("__rawX", xMin, xMax, profile.width, 0)
78
+ : mapCall("__rawX", xMin, xMax, 0, profile.width);
74
79
  mapY = invertY
75
- ? `map(__rawY, ${yMin}, ${yMax}, ${profile.height}, 0)`
76
- : `map(__rawY, ${yMin}, ${yMax}, 0, ${profile.height})`;
80
+ ? mapCall("__rawY", yMin, yMax, profile.height, 0)
81
+ : mapCall("__rawY", yMin, yMax, 0, profile.height);
77
82
  }
78
83
  else {
79
- mapX = `map(__rawY, ${yMin}, ${yMax}, ${profile.width}, 0)`;
80
- mapY = `map(__rawX, ${xMin}, ${xMax}, 0, ${profile.height})`;
84
+ mapX = mapCall("__rawY", yMin, yMax, profile.width, 0);
85
+ mapY = mapCall("__rawX", xMin, xMax, 0, profile.height);
81
86
  }
82
87
  }
83
88
  else {
@@ -323,8 +328,12 @@ export function emitUIRuntime(ctx) {
323
328
  ctx.sourceLines.push(lowered.keyboardDispatch);
324
329
  }
325
330
  // 3. Signal variables (one per ui.signal / const X = ui.signal).
331
+ // A3-9-1: signal decls bypass the statement renderer, so apply the
332
+ // int -> fixed-width substitution here under --autosar.
333
+ const signalAutosarOn = ctx.compliance.isEnabled() && ctx.compliance.isBanned("A3-9-1");
334
+ const fixedWidth = signalAutosarOn && ctx.strategy ? ctx.strategy.defaultNumericType(ctx.compliance) : "";
326
335
  for (const decl of uiSignalDecls()) {
327
- ctx.sourceLines.push(decl);
336
+ ctx.sourceLines.push(fixedWidth && decl.startsWith("int ") ? `${fixedWidth}${decl.slice(3)}` : decl);
328
337
  }
329
338
  // 4. Binding table (accumulated from ui.bind calls).
330
339
  // Forward-declare the binding compute functions first: the table references
@@ -462,17 +471,33 @@ export function emitUIRuntime(ctx) {
462
471
  }
463
472
  return entries.join(", ");
464
473
  };
474
+ // Per-table counts: the runtime indexes each table by node, so the safe
475
+ // bound for dispatch is each table's own highest populated index + 1 — not
476
+ // the shared click-table size (holds/releases can span fewer nodes).
477
+ const tableCount = (kind) => {
478
+ const max = touchHandlers
479
+ .filter(h => h.kind === kind)
480
+ .reduce((m, h) => Math.max(m, h.nodeIndex), -1);
481
+ return Math.max(max + 1, 1);
482
+ };
483
+ // The handler tables are link-time constants (function pointers only) and
484
+ // are never written at runtime — emit them const so they land in flash
485
+ // rodata instead of stealing DRAM (~4.5KB on a 380-node demo).
465
486
  if (profile.touch) {
466
- ctx.sourceLines.push(`void (*__ui_click_handlers[])() = { ${buildTable("click")} };`);
467
- ctx.sourceLines.push(`void (*__ui_hold_handlers[])() = { ${buildTable("hold")} };`);
468
- ctx.sourceLines.push(`void (*__ui_release_handlers[])() = { ${buildTable("release")} };`);
487
+ ctx.sourceLines.push(`void (*const __ui_click_handlers[])() = { ${buildTable("click")} };`);
488
+ ctx.sourceLines.push(`void (*const __ui_hold_handlers[])() = { ${buildTable("hold")} };`);
489
+ ctx.sourceLines.push(`void (*const __ui_release_handlers[])() = { ${buildTable("release")} };`);
469
490
  ctx.sourceLines.push(`const uint16_t __ui_click_handler_count = ${tableSize};`);
491
+ ctx.sourceLines.push(`const uint16_t __ui_hold_handler_count = ${tableCount("hold")};`);
492
+ ctx.sourceLines.push(`const uint16_t __ui_release_handler_count = ${tableCount("release")};`);
470
493
  }
471
494
  else {
472
- ctx.sourceLines.push(`void (*__ui_click_handlers[])() = {};`);
473
- ctx.sourceLines.push(`void (*__ui_hold_handlers[])() = {};`);
474
- ctx.sourceLines.push(`void (*__ui_release_handlers[])() = {};`);
495
+ ctx.sourceLines.push(`void (*const __ui_click_handlers[])() = {};`);
496
+ ctx.sourceLines.push(`void (*const __ui_hold_handlers[])() = {};`);
497
+ ctx.sourceLines.push(`void (*const __ui_release_handlers[])() = {};`);
475
498
  ctx.sourceLines.push(`const uint16_t __ui_click_handler_count = 0;`);
499
+ ctx.sourceLines.push(`const uint16_t __ui_hold_handler_count = 0;`);
500
+ ctx.sourceLines.push(`const uint16_t __ui_release_handler_count = 0;`);
476
501
  }
477
502
  // 8b. Input onChange dispatch — assigns __ui_kb_onchange based on __ui_kb_target.
478
503
  // Forward-declare __ui_kb_set_onchange unconditionally: the runtime header's
@@ -8,8 +8,20 @@
8
8
  // everything else goes to resolveHALOperation (the existing generic seam).
9
9
  // Keeps the consumer sites (expression-renderer, statement-renderer,
10
10
  // render-expr) DRY.
11
+ //
12
+ // Output-pin state tracking is also handled here, centrally, so every
13
+ // strategy benefits without per-strategy changes:
14
+ // - gpio.read with `trackedValue` never reaches the strategy (a hardware
15
+ // read of a direction-only output is not portable, e.g. Zephyr); it folds
16
+ // to a constant or the tracked shadow variable.
17
+ // - gpio.write / gpio.toggle flagged `updatesShadow` get the shadow variable
18
+ // update appended to whatever the strategy produced. The flag is baked
19
+ // into the op at IR-build time (markShadowUpdatingOps) — emit must not
20
+ // consult live tracker state, because all files build (each resetting the
21
+ // tracker) before any file emits.
11
22
  // ---------------------------------------------------------------------------
12
23
  import { getSafetyHook } from "../safety-hook.js";
24
+ import { pinShadowVarName } from "../ir/pin-state-tracking.js";
13
25
  export function routeHALOp(op, strategy) {
14
26
  if (typeof op.operation === "string") {
15
27
  if (op.operation.startsWith("display.")) {
@@ -22,5 +34,47 @@ export function routeHALOp(op, strategy) {
22
34
  return getSafetyHook()?.resolveSafetyOp?.(op);
23
35
  }
24
36
  }
25
- return strategy.resolveHALOperation?.(op);
37
+ if (op.operation === "gpio.read" && op.trackedValue) {
38
+ // Tracked OUTPUT-pin read: software truth, never a hardware read.
39
+ if (op.trackedValue === "high")
40
+ return { expression: "true" };
41
+ if (op.trackedValue === "low")
42
+ return { expression: "false" };
43
+ return { expression: pinShadowVarName(op.pin) };
44
+ }
45
+ const resolved = strategy.resolveHALOperation?.(op);
46
+ // Toggle on a shadow-tracked pin must not use the strategy's default
47
+ // read-modify-write form (e.g. digitalWrite(p, digitalRead(p) ...) on
48
+ // Arduino) — reading the pin back is exactly what tracking avoids. Lower
49
+ // it as a write of the shadow's current value, then flip the shadow.
50
+ if (op.operation === "gpio.toggle" && op.updatesShadow) {
51
+ const varName = pinShadowVarName(op.pin);
52
+ const writeOp = {
53
+ operation: "gpio.write",
54
+ pin: op.pin,
55
+ value: varName,
56
+ ...(op.port !== undefined ? { port: op.port } : {}),
57
+ };
58
+ const writeResolved = strategy.resolveHALOperation?.(writeOp);
59
+ const flip = `${varName} = (!${varName});`;
60
+ if (writeResolved?.code) {
61
+ return { ...writeResolved, code: `${writeResolved.code}\n${flip}` };
62
+ }
63
+ if (writeResolved?.expression) {
64
+ return { ...writeResolved, expression: `${writeResolved.expression}, ${flip}` };
65
+ }
66
+ return { code: flip };
67
+ }
68
+ if (op.operation === "gpio.write" && op.updatesShadow) {
69
+ const varName = pinShadowVarName(op.pin);
70
+ const update = `${varName} = ((${op.value}) != 0);`;
71
+ if (resolved?.code) {
72
+ return { ...resolved, code: `${resolved.code}\n${update}` };
73
+ }
74
+ if (resolved?.expression) {
75
+ return { ...resolved, expression: `${resolved.expression}, ${update}` };
76
+ }
77
+ return { code: update };
78
+ }
79
+ return resolved;
26
80
  }
@@ -551,8 +551,11 @@ export class StatementRenderer {
551
551
  }
552
552
  const declaredType = this.normalizeCppType(statement.cppType);
553
553
  const volatilePrefix = statement.isVolatile ? "volatile " : "";
554
- // Transform type name for Arduino library classes (add namespace prefix)
555
- const transformedType = transformTypeName(statement.cppType, this.classNameMap);
554
+ // Transform type name for Arduino library classes (add namespace prefix).
555
+ // A3-9-1: substitute the fixed-width default for the IR's hardcoded "int"
556
+ // BEFORE the class-name transform so var declarations match the other
557
+ // declaration paths under --autosar.
558
+ const transformedType = transformTypeName(declaredType, this.classNameMap);
556
559
  const ownershipKind = statement.ownershipKind;
557
560
  // Emit const for Shared<T> ownership annotations (ownershipKind === 'shared')
558
561
  const isConst = statement.storage === "const" || ownershipKind === 'shared';
@@ -8,7 +8,8 @@ import { buildFunctionReturnTypeMap } from "./type-resolution.js";
8
8
  import { resolveBoardConstants, tryResolveBoardDefFile } from "./board-resolver.js";
9
9
  import { analyzePeripheralUsage, createEmptyPeripheralUsage } from "./peripheral-usage.js";
10
10
  import { runProgramValidations } from "./validation-orchestrator.js";
11
- import { registerFieldMap, hoistedNestedFunctions, hoistedNestedClasses, hoistedNestedEnums, hoistedNestedInterfaces, hoistedNestedTypeAliases, activeNamespaceNames, activeEnumNames, activeStringEnumNames, peripheralAliasMap, pinAliasMap, mcuPinReverseMap, topLevelClassNames, topLevelInterfaceNames, classTypeNames, topLevelClasses, requiredIncludes, resetBuildState, getCurrentBoardConstants, setCurrentBoardConstants, contextStorage, CompilationContext, registeredCallbacks, getContext, discriminatedUnionVariantNames, restParamFunctions, topLevelAliasReceivers } from "./build-ir-state.js";
11
+ import { registerFieldMap, hoistedNestedFunctions, hoistedNestedClasses, hoistedNestedEnums, hoistedNestedInterfaces, hoistedNestedTypeAliases, activeNamespaceNames, activeEnumNames, activeStringEnumNames, peripheralAliasMap, pinAliasMap, mcuPinForwardMap, mcuPinReverseMap, topLevelClassNames, topLevelInterfaceNames, classTypeNames, topLevelClasses, requiredIncludes, resetBuildState, getCurrentBoardConstants, setCurrentBoardConstants, contextStorage, CompilationContext, registeredCallbacks, getContext, discriminatedUnionVariantNames, restParamFunctions, topLevelAliasReceivers } from "./build-ir-state.js";
12
+ import { pinShadowVarName, takeShadowDeclarations, resetPinStateTracking, markShadowUpdatingOps } from "./pin-state-tracking.js";
12
13
  import { collectPointerVars, expressionStatementToIR, lowerStatement, variableStatementToIR, prescanArrayUsage, lowerStatementList } from "./statement-to-ir.js";
13
14
  import { registerUIModuleImport, registerElementValue } from "./transformers/ui-call-resolver.js";
14
15
  import { requireUIHook } from "../ui-hook.js";
@@ -181,6 +182,7 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
181
182
  // Reset module-level state for this file
182
183
  resetBuildState();
183
184
  resetHALResolver();
185
+ resetPinStateTracking();
184
186
  registerFieldMap.clear();
185
187
  // Phase 0: Pre-scan for top-level classes and register them so type inference can resolve them.
186
188
  // Also register classes from other files in the transpile graph so that property accesses
@@ -495,10 +497,31 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
495
497
  halInstances.set(name, { className: "Pin", fieldValues: fields });
496
498
  continue;
497
499
  }
498
- // A-pins: A0-A19Pin instance with board-specific offset and MCU port name (fallback)
500
+ // A-pins: A0, A1, … resolve through the board manifest's analog
501
+ // pin list first (A-ordered on every board package: A<n> →
502
+ // pins.analog[n] → the MCU port name → pin number). Only when the
503
+ // list is absent (bare Arduino-style boards) fall back to the
504
+ // analogOffset convention (A0 = 14) — on port-named boards that
505
+ // fallback resolves A1 to a random GPIO (Black Pill PA15, a
506
+ // non-analog pin, was the first to surface it via a capability
507
+ // error).
499
508
  const aMatch = name.match(/^A(\d+)$/);
500
509
  if (aMatch) {
501
- const pinNum = String(analogOffset + parseInt(aMatch[1]));
510
+ let pinNum;
511
+ // The flattener stores the manifest's analog pin list as a
512
+ // comma-joined string under `pins.analog`.
513
+ const analogList = boardConstants.get("pins.analog");
514
+ if (typeof analogList === "string") {
515
+ const analogName = analogList.split(",")[parseInt(aMatch[1], 10)];
516
+ if (analogName) {
517
+ const byName = mcuPinForwardMap.get(analogName);
518
+ if (byName !== undefined)
519
+ pinNum = String(byName);
520
+ }
521
+ }
522
+ if (pinNum === undefined) {
523
+ pinNum = String(analogOffset + parseInt(aMatch[1]));
524
+ }
502
525
  const fields = new Map([["_pin", pinNum]]);
503
526
  const portName = mcuPinReverseMap.get(pinNum);
504
527
  if (portName)
@@ -786,7 +809,7 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
786
809
  // If peripheral analysis fails, use empty usage
787
810
  peripheralUsage = createEmptyPeripheralUsage();
788
811
  }
789
- return {
812
+ const program = {
790
813
  fileName,
791
814
  imports,
792
815
  reExports,
@@ -808,5 +831,24 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
808
831
  restParamFunctions: new Map(restParamFunctions),
809
832
  ...(defaultExportName ? { defaultExportName } : {}),
810
833
  };
834
+ // Output-pin state tracking. First bake the shadow-update flags into the
835
+ // write/toggle ops, while this file's tracker state is still live (the
836
+ // next file's build resets it, and emit runs after every file is built —
837
+ // multi-file programs would lose the updates otherwise). Then consume the
838
+ // shadow declarations — pins whose reads lowered to the shadow variable
839
+ // form need a file-scope declaration.
840
+ markShadowUpdatingOps(program);
841
+ const shadowDecls = takeShadowDeclarations();
842
+ for (const { pin, initial } of shadowDecls) {
843
+ program.topLevelStatements.unshift({
844
+ kind: "var_decl",
845
+ sourceSpan: { filePath: fileName, startOffset: 0, endOffset: 0, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0 },
846
+ name: pinShadowVarName(pin),
847
+ storage: "var",
848
+ cppType: "bool",
849
+ initializer: { kind: "boolean", value: initial },
850
+ });
851
+ }
852
+ return program;
811
853
  });
812
854
  }
@@ -1,6 +1,7 @@
1
1
  import ts from "typescript";
2
2
  import { makeDiagnostic, makeSourceSpan } from "./ast-node-utils.js";
3
3
  import { PIN_FACTORY_FUNCTIONS, CONSTANT_FOLD_FUNCTIONS, TYPED_ARRAY_ELEMENT_MAP, activeCArrayVars, activeArrayLiteralVars, activeStringVars, nestedFunctionAliases, nestedClassAliases, hoistedNestedClasses, mutableArrayVars, arrayLiteralSizes, filteredArrayLengthVars, activeNamespaceNames, activeEnumNames, activeStringEnumNames, topLevelClassNames, topLevelInterfaceNames, classTypeNames, topLevelClasses, getActiveExtendsClass, restParamFunctions, getContext, getCurrentBoardConstants } from "./build-ir-state.js";
4
+ import { isPinFoldingEnabled, setPinFoldingEnabled } from "./pin-state-tracking.js";
4
5
  import { getCurrentIrTypeScope } from "./symbol-types.js";
5
6
  import { renderExprAsText } from "./render-expr.js";
6
7
  import { lowerStatement, tryResolveHALExpression } from "./statement-to-ir.js";
@@ -340,6 +341,17 @@ export function expressionToIR(expr, sourceText, diagnostics, pointerVars = new
340
341
  * variable name prefixed with "__FILTERED_LEN__" so the caller can detect it.
341
342
  */
342
343
  function resolveLengthProperty(receiverNode, objectText) {
344
+ // UI element text: screen.<id>.text is a raw char buffer on the node —
345
+ // `.length` must be strlen, not `.size()` (char[33] has no size member).
346
+ if (ts.isPropertyAccessExpression(receiverNode) &&
347
+ receiverNode.name.text === "text" &&
348
+ ts.isPropertyAccessExpression(receiverNode.expression) &&
349
+ ts.isIdentifier(receiverNode.expression.expression)) {
350
+ const nodeIdx = resolveElementValue(receiverNode.expression.expression.text, receiverNode.expression.name.text);
351
+ if (nodeIdx !== undefined) {
352
+ return `static_cast<long long>(strlen(__ui_nodes[${nodeIdx}].textBuffer))`;
353
+ }
354
+ }
343
355
  if (ts.isStringLiteral(receiverNode) || ts.isNoSubstitutionTemplateLiteral(receiverNode)) {
344
356
  return `${receiverNode.text.length}`;
345
357
  }
@@ -2185,6 +2197,10 @@ export function expressionToIR(expr, sourceText, diagnostics, pointerVars = new
2185
2197
  }
2186
2198
  }
2187
2199
  const isBlock = ts.isBlock(body);
2200
+ // Lambda bodies run at an unmodeled time (callbacks), so pin-state
2201
+ // constant folding must be off inside them.
2202
+ const prevPinFolding = isPinFoldingEnabled();
2203
+ setPinFoldingEnabled(false);
2188
2204
  const bodyStmts = isBlock
2189
2205
  ? body.statements.map(stmt => {
2190
2206
  const lowered = lowerStatement(stmt, "", sourceText, diagnostics, new Map(), new Map(), "<lambda>", new Map(), pointerVars);
@@ -2196,6 +2212,7 @@ export function expressionToIR(expr, sourceText, diagnostics, pointerVars = new
2196
2212
  sourceSpan: makeSourceSpan(body, "", sourceText),
2197
2213
  value: expressionToIR(body, sourceText, diagnostics, pointerVars),
2198
2214
  }];
2215
+ setPinFoldingEnabled(prevPinFolding);
2199
2216
  // Infer return type: use explicit annotation, or infer from body. Thread
2200
2217
  // the lambda's own param types into the inference so a body like
2201
2218
  // `(n) => n.capacity` can resolve `n` and deduce the return type.
@@ -5,6 +5,7 @@ import { renderExprAsText } from "../render-expr.js";
5
5
  import { escapeCppKeyword } from "../../utils/strings.js";
6
6
  import { halClassRegistry, halGlobalFunctions } from "./hal-parser.js";
7
7
  import { tryResolveSemanticCall, tryResolveBoardResolveArg, tryResolveCompoundSemanticReturn, resolveConcatPath } from "./hal-plugins.js";
8
+ import { resolveTrackedRead, pinShadowVarName } from "../pin-state-tracking.js";
8
9
  import { cppTypeForHalOp } from "../../emit/utils/hal-op-cpp-type.js";
9
10
  /** Escape C++ keywords in resolved text, but only when the text looks like a
10
11
  * variable reference (not a literal like "false", "true", "42", or a string). */
@@ -58,13 +59,30 @@ function prefixOperatorText(operator) {
58
59
  function inlineThisGetterCall(methodName, pin, strategy) {
59
60
  switch (methodName) {
60
61
  case "read":
61
- return strategy?.readDigitalPin?.(pin) ?? `digitalRead(${pin})`;
62
- case "readAnalog":
63
- return strategy?.readAnalogPin?.(pin) ?? `analogRead(${pin})`;
64
62
  case "isHigh":
65
- return strategy?.readDigitalPin?.(pin) ?? `digitalRead(${pin})`;
66
- case "isLow":
63
+ case "isLow": {
64
+ // Output-pin state tracking: when the receiver is a tracked OUTPUT pin,
65
+ // lower to the tracked level (constant when statically known, shadow
66
+ // variable otherwise) instead of a hardware read. Reading back a
67
+ // direction-only output is not portable (e.g. Zephyr).
68
+ const pinNum = Number.parseInt(pin, 10);
69
+ if (Number.isFinite(pinNum)) {
70
+ const tracked = resolveTrackedRead(pinNum);
71
+ if (tracked !== null) {
72
+ const levelText = tracked === "shadow"
73
+ ? pinShadowVarName(pinNum)
74
+ : (tracked === "high" ? "true" : "false");
75
+ return methodName === "isLow" ? `(!${levelText})` : levelText;
76
+ }
77
+ }
78
+ if (methodName === "read")
79
+ return strategy?.readDigitalPin?.(pin) ?? `digitalRead(${pin})`;
80
+ if (methodName === "isHigh")
81
+ return strategy?.readDigitalPin?.(pin) ?? `digitalRead(${pin})`;
67
82
  return strategy?.readDigitalPin ? `(!${strategy.readDigitalPin(pin)})` : `(!digitalRead(${pin}))`;
83
+ }
84
+ case "readAnalog":
85
+ return strategy?.readAnalogPin?.(pin) ?? `analogRead(${pin})`;
68
86
  default:
69
87
  return null;
70
88
  }
@@ -3,6 +3,7 @@ import { getCurrentBoardConstants, halInstances, getContext } from "../build-ir-
3
3
  import { resolveExpressionText } from "./hal-emitter.js";
4
4
  import { renderExprAsText } from "../render-expr.js";
5
5
  import { hasSafetyHook, requireSafetyHook } from "../../safety-hook.js";
6
+ import { notePinSetMode, notePinToggle, notePinWrite, notePinAnalogOutput, resolveTrackedRead } from "../pin-state-tracking.js";
6
7
  /**
7
8
  * Split a comma-joined argument list back into individual arguments, respecting
8
9
  * nesting (parens/brackets/braces) and string literals so a comma inside one of
@@ -326,11 +327,13 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
326
327
  // Try literal resolution first (compile-time 0/1/true/false)
327
328
  const numValue = resolveNumericArg(args, 1, instance, paramNames, callArgTexts, paramDefaults);
328
329
  if (numValue !== null) {
330
+ notePinWrite(pin, numValue);
329
331
  return { operation: "gpio.write", port, pin, value: (numValue ? 1 : 0) };
330
332
  }
331
333
  // Fall back to runtime expression (e.g. a variable, negated expression)
332
334
  const exprValue = resolveSemanticArg(args, 1, instance, paramNames, callArgTexts, paramDefaults);
333
335
  if (exprValue !== null) {
336
+ notePinWrite(pin, null);
334
337
  return { operation: "gpio.write", port, pin, value: exprValue };
335
338
  }
336
339
  return null;
@@ -339,12 +342,17 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
339
342
  const pin = resolveNumericArg(args, 0, instance, paramNames, callArgTexts, paramDefaults);
340
343
  if (pin === null)
341
344
  return null;
345
+ const tracked = resolveTrackedRead(pin);
346
+ if (tracked !== null) {
347
+ return { operation: "gpio.read", port, pin, trackedValue: tracked };
348
+ }
342
349
  return { operation: "gpio.read", port, pin };
343
350
  }
344
351
  case "gpioToggle": {
345
352
  const pin = resolveNumericArg(args, 0, instance, paramNames, callArgTexts, paramDefaults);
346
353
  if (pin === null)
347
354
  return null;
355
+ notePinToggle(pin);
348
356
  return { operation: "gpio.toggle", port, pin };
349
357
  }
350
358
  case "gpioSetMode": {
@@ -352,6 +360,7 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
352
360
  const mode = resolveSemanticArg(args, 1, instance, paramNames, callArgTexts, paramDefaults);
353
361
  if (pin === null || mode === null)
354
362
  return null;
363
+ notePinSetMode(pin, mode);
355
364
  return { operation: "gpio.set_mode", port, pin, mode };
356
365
  }
357
366
  // ── PWM ──
@@ -360,6 +369,7 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
360
369
  const duty = resolveNumericOrExpression(args, 1, instance, paramNames, callArgTexts, paramDefaults);
361
370
  if (pin === null || duty === null)
362
371
  return null;
372
+ notePinAnalogOutput(pin);
363
373
  return { operation: "pwm.write", port, pin, duty };
364
374
  }
365
375
  // ── RMT ──
@@ -1127,6 +1137,7 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
1127
1137
  const duration = resolveNumericOrExpression(args, 2, instance, paramNames, callArgTexts, paramDefaults);
1128
1138
  if (pin === null || frequency === null)
1129
1139
  return null;
1140
+ notePinAnalogOutput(pin);
1130
1141
  return { operation: "tone.play", port, pin, frequency, ...(duration !== null ? { duration } : {}) };
1131
1142
  }
1132
1143
  case "toneStop": {
@@ -34,6 +34,9 @@ export function validatePinModeConfig(program) {
34
34
  const diagnostics = [];
35
35
  // Track which receivers have had their mode explicitly set
36
36
  const pinModeSet = new Set();
37
+ // Pins currently driven by PWM/tone (since their last digital write or
38
+ // mode change). Reading such a pin has no defined digital level.
39
+ const analogDrivenPins = new Set();
37
40
  const checkCuttlefishCall = (receiver, receiverKind, method) => {
38
41
  if (!receiverKind || !PIN_RECEIVER_KINDS.has(receiverKind))
39
42
  return;
@@ -98,16 +101,36 @@ export function validatePinModeConfig(program) {
98
101
  const pinKey = `pin${op.pin}`;
99
102
  if (op.operation === 'gpio.set_mode') {
100
103
  pinModeSet.add(pinKey);
104
+ analogDrivenPins.delete(pinKey);
101
105
  }
102
- else if (op.operation === 'gpio.read' && !pinModeSet.has(pinKey)) {
103
- diagnostics.push({
104
- severity: 'warning',
105
- message: `Pin ${op.pin} read without prior mode configuration. ` +
106
- `Call asInput() or inputPullUp() first — reading a floating pin is undefined behavior.`,
107
- filePath: program.fileName,
108
- code: 'pin-mode-not-set',
109
- source: 'pin-mode-validation',
110
- });
106
+ else if (op.operation === 'pwm.write' || op.operation === 'tone.play') {
107
+ analogDrivenPins.add(pinKey);
108
+ }
109
+ else if (op.operation === 'gpio.write' || op.operation === 'gpio.toggle') {
110
+ analogDrivenPins.delete(pinKey);
111
+ }
112
+ else if (op.operation === 'gpio.read') {
113
+ if (analogDrivenPins.has(pinKey)) {
114
+ diagnostics.push({
115
+ severity: 'warning',
116
+ message: `Pin ${op.pin} read while driven by PWM/tone. ` +
117
+ `The pin has no defined digital level while an analog output is active; ` +
118
+ `tracked reads return the last digital write, not the waveform.`,
119
+ filePath: program.fileName,
120
+ code: 'pin-read-while-pwm',
121
+ source: 'pin-mode-validation',
122
+ });
123
+ }
124
+ else if (!pinModeSet.has(pinKey)) {
125
+ diagnostics.push({
126
+ severity: 'warning',
127
+ message: `Pin ${op.pin} read without prior mode configuration. ` +
128
+ `Call asInput() or inputPullUp() first — reading a floating pin is undefined behavior.`,
129
+ filePath: program.fileName,
130
+ code: 'pin-mode-not-set',
131
+ source: 'pin-mode-validation',
132
+ });
133
+ }
111
134
  }
112
135
  else if ((op.operation === 'gpio.write' || op.operation === 'gpio.toggle') && !pinModeSet.has(pinKey)) {
113
136
  diagnostics.push({