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

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 (54) hide show
  1. package/dist/add-preset.d.ts +4 -0
  2. package/dist/add-preset.js +74 -0
  3. package/dist/api/config.d.ts +0 -4
  4. package/dist/api/shared/display-adapters/sdl.js +1 -1
  5. package/dist/api/shared/display-profile.d.ts +11 -0
  6. package/dist/api/shared/display-profile.js +3 -0
  7. package/dist/api/shared/hal-op-ir.d.ts +19 -0
  8. package/dist/api/shared/toolchain-types.d.ts +0 -1
  9. package/dist/cli.js +15 -4
  10. package/dist/config-loader.d.ts +0 -2
  11. package/dist/config-loader.js +10 -5
  12. package/dist/config-schema.d.ts +87 -94
  13. package/dist/config-schema.js +0 -3
  14. package/dist/create/debug-artifacts.d.ts +20 -0
  15. package/dist/create/debug-artifacts.js +69 -0
  16. package/dist/create/index.d.ts +2 -0
  17. package/dist/create/index.js +1 -0
  18. package/dist/create/init-scaffold.d.ts +1 -0
  19. package/dist/create/init-scaffold.js +5 -0
  20. package/dist/create/init-templates.js +0 -2
  21. package/dist/emit/compliance/rules.js +18 -4
  22. package/dist/emit/emitters/function-emitter-impl.js +7 -1
  23. package/dist/emit/emitters/line-appender.js +6 -0
  24. package/dist/emit/emitters/ui-emitter.js +40 -15
  25. package/dist/emit/route-hal-op.js +55 -1
  26. package/dist/emit/statement-renderer.js +5 -2
  27. package/dist/ir/build-ir.js +22 -1
  28. package/dist/ir/expression-to-ir.js +17 -0
  29. package/dist/ir/hal/hal-emitter.js +23 -5
  30. package/dist/ir/hal/hal-plugins.js +11 -0
  31. package/dist/ir/pin-mode-validation.js +32 -9
  32. package/dist/ir/pin-state-tracking.d.ts +58 -0
  33. package/dist/ir/pin-state-tracking.js +182 -0
  34. package/dist/ir/program-analysis.d.ts +6 -0
  35. package/dist/ir/program-analysis.js +38 -0
  36. package/dist/ir/statement-to-ir.js +14 -0
  37. package/dist/ir/transformers/control-flow.js +29 -0
  38. package/dist/ir/transformers/ui-call-resolver.js +105 -1
  39. package/dist/ir/ui-element-auto-wire.js +7 -4
  40. package/dist/orchestrator/graph-builder.d.ts +4 -1
  41. package/dist/orchestrator/graph-builder.js +7 -1
  42. package/dist/preview/api-shared-shim.d.ts +1 -0
  43. package/dist/preview/api-shared-shim.js +7 -0
  44. package/dist/preview/client.js +220 -1
  45. package/dist/preview/server.js +154 -62
  46. package/dist/theme-tokens.d.ts +22 -0
  47. package/dist/theme-tokens.js +172 -0
  48. package/dist/transpile.js +35 -5
  49. package/dist/types.d.ts +5 -0
  50. package/dist/ui-hook.d.ts +7 -0
  51. package/dist/utils/cli.js +9 -0
  52. package/dist/utils/ui.d.ts +5 -0
  53. package/dist/utils/ui.js +7 -0
  54. package/package.json +7 -6
@@ -0,0 +1,69 @@
1
+ // ---------------------------------------------------------------------------
2
+ // debug-artifacts.ts — create-time framework debug profile generation
3
+ //
4
+ // `cuttlefish create` scaffolds a project that has never been built, so the
5
+ // framework toolchain's post-build debug-artifact writer (e.g. Zephyr's
6
+ // writeDebugConfig, which runs after a successful `west build --debug`) has
7
+ // never had a chance to run. Without a .vscode/launch.json, pressing F5 in VS
8
+ // Code just opens the "select a debugger" menu.
9
+ //
10
+ // Frameworks that support native debugging may export
11
+ // `writeProjectDebugArtifacts({ workspaceRoot, buildTarget })` from their
12
+ // package root (see @typecad/framework-zephyr). This helper loads the freshly
13
+ // installed framework package and invokes that export — the framework decides
14
+ // whether the target is debug-capable and what to write. Frameworks without
15
+ // the export (or targets without native debug support) are a silent no-op.
16
+ //
17
+ // Everything here is best-effort: a failed or skipped generation never fails
18
+ // `cuttlefish create`, because the first `--debug` build still writes the
19
+ // artifacts the hard way.
20
+ // ---------------------------------------------------------------------------
21
+ import path from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { loadFrameworkPackage } from "../framework-package.js";
24
+ const cliModuleDir = path.dirname(fileURLToPath(import.meta.url));
25
+ /**
26
+ * Generate the framework's starter debug artifacts for a new project.
27
+ * Returns the workspace-relative paths written (e.g. ['.vscode/launch.json']),
28
+ * or [] when the framework has no debug support, isn't resolvable yet, or the
29
+ * generator failed (warns, never throws).
30
+ */
31
+ export function generateFrameworkDebugArtifacts(o, loadModule) {
32
+ if (!o.frameworkPackage || !o.workspaceRoot)
33
+ return [];
34
+ // Default loader: prefer the new project's node_modules (populated by the
35
+ // create install step), then the CLI's own module location (monorepo /
36
+ // --no-install flows where the framework is a workspace sibling of the
37
+ // running cuttlefish).
38
+ const loader = loadModule ?? ((packageName) => {
39
+ for (const dir of [o.workspaceRoot, cliModuleDir]) {
40
+ try {
41
+ return loadFrameworkPackage(packageName, dir);
42
+ }
43
+ catch {
44
+ // not resolvable from this location — try the next
45
+ }
46
+ }
47
+ return undefined;
48
+ });
49
+ let mod;
50
+ try {
51
+ mod = loader(o.frameworkPackage);
52
+ }
53
+ catch {
54
+ return [];
55
+ }
56
+ if (!mod || typeof mod.writeProjectDebugArtifacts !== "function")
57
+ return [];
58
+ try {
59
+ const written = mod.writeProjectDebugArtifacts({
60
+ workspaceRoot: o.workspaceRoot,
61
+ buildTarget: o.buildTarget,
62
+ });
63
+ return Array.isArray(written) ? written.map(String) : [];
64
+ }
65
+ catch (e) {
66
+ console.warn(`! Debug profile generation failed: ${e.message}`);
67
+ return [];
68
+ }
69
+ }
@@ -7,6 +7,8 @@ export type { ProjectInstallResult } from './install-deps.js';
7
7
  export { generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, } from './init-templates.js';
8
8
  export type { InitProjectOptions } from './init-templates.js';
9
9
  export { runInitWizard } from './init-wizard.js';
10
+ export { generateFrameworkDebugArtifacts } from './debug-artifacts.js';
11
+ export type { FrameworkDebugArtifactsOptions, FrameworkModuleLoader } from './debug-artifacts.js';
10
12
  export { scaffoldBoardPackages } from './board-codegen.js';
11
13
  export type { ScaffoldBoardResult } from './board-codegen.js';
12
14
  export { generateFrameworkChecklist } from './board-checklist.js';
@@ -3,6 +3,7 @@ export { FRAMEWORK_CATALOG, frameworksForTarget, frameworkCatalogEntry, detectPa
3
3
  export { installProjectDependencies, __setProjectInstallRunnerForTest } from './install-deps.js';
4
4
  export { generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, } from './init-templates.js';
5
5
  export { runInitWizard } from './init-wizard.js';
6
+ export { generateFrameworkDebugArtifacts } from './debug-artifacts.js';
6
7
  // Board codegen tool (`cuttlefish board add`)
7
8
  export { scaffoldBoardPackages } from './board-codegen.js';
8
9
  export { generateFrameworkChecklist } from './board-checklist.js';
@@ -33,4 +33,5 @@ export interface ScaffoldProjectResult {
33
33
  export declare function scaffoldProject(options: InitProjectOptions, outDir?: string): ScaffoldProjectResult;
34
34
  export declare function printInitNextSteps(options: InitProjectOptions, outDir: string, opts?: {
35
35
  installed?: boolean;
36
+ debugProfile?: boolean;
36
37
  }): void;
@@ -168,6 +168,11 @@ export function printInitNextSteps(options, outDir, opts = {}) {
168
168
  console.log(chalk.dim(` npm install`));
169
169
  }
170
170
  console.log(` ${chalk.cyan("npm run compile")}`);
171
+ if (opts.debugProfile) {
172
+ console.log();
173
+ console.log(chalk.bold.white("To debug (VS Code):"));
174
+ console.log(` ${chalk.cyan("open the folder and press F5")} ${chalk.dim("(builds + flashes, then attaches GDB)")}`);
175
+ }
171
176
  if (!options.isNative) {
172
177
  const portHint = process.platform === 'win32' ? 'COM4' : '/dev/ttyACM0';
173
178
  console.log();
@@ -149,7 +149,6 @@ const config = {
149
149
 
150
150
  // Output options
151
151
  output: {
152
- optimize: 'speed',
153
152
  outDir: './out',
154
153
  },
155
154
  };
@@ -198,7 +197,6 @@ const config: CuttlefishConfig = {
198
197
  // Output / build options
199
198
  output: {
200
199
  framework: '${options.framework}',
201
- optimize: 'size',
202
200
  outDir: './out',
203
201
  },
204
202
 
@@ -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)
@@ -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';
@@ -9,6 +9,7 @@ import { resolveBoardConstants, tryResolveBoardDefFile } from "./board-resolver.
9
9
  import { analyzePeripheralUsage, createEmptyPeripheralUsage } from "./peripheral-usage.js";
10
10
  import { runProgramValidations } from "./validation-orchestrator.js";
11
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";
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
@@ -786,7 +788,7 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
786
788
  // If peripheral analysis fails, use empty usage
787
789
  peripheralUsage = createEmptyPeripheralUsage();
788
790
  }
789
- return {
791
+ const program = {
790
792
  fileName,
791
793
  imports,
792
794
  reExports,
@@ -808,5 +810,24 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
808
810
  restParamFunctions: new Map(restParamFunctions),
809
811
  ...(defaultExportName ? { defaultExportName } : {}),
810
812
  };
813
+ // Output-pin state tracking. First bake the shadow-update flags into the
814
+ // write/toggle ops, while this file's tracker state is still live (the
815
+ // next file's build resets it, and emit runs after every file is built —
816
+ // multi-file programs would lose the updates otherwise). Then consume the
817
+ // shadow declarations — pins whose reads lowered to the shadow variable
818
+ // form need a file-scope declaration.
819
+ markShadowUpdatingOps(program);
820
+ const shadowDecls = takeShadowDeclarations();
821
+ for (const { pin, initial } of shadowDecls) {
822
+ program.topLevelStatements.unshift({
823
+ kind: "var_decl",
824
+ sourceSpan: { filePath: fileName, startOffset: 0, endOffset: 0, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0 },
825
+ name: pinShadowVarName(pin),
826
+ storage: "var",
827
+ cppType: "bool",
828
+ initializer: { kind: "boolean", value: initial },
829
+ });
830
+ }
831
+ return program;
811
832
  });
812
833
  }
@@ -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({