@typecad/cuttlefish 1.0.0-alpha.11 → 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 (65) 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 +5 -5
  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/framework-manifest.d.ts +85 -78
  8. package/dist/api/shared/framework-manifest.js +1 -0
  9. package/dist/api/shared/hal-op-ir.d.ts +19 -0
  10. package/dist/api/shared/toolchain-types.d.ts +0 -1
  11. package/dist/cli.js +21 -4
  12. package/dist/config-loader.d.ts +8 -2
  13. package/dist/config-loader.js +202 -53
  14. package/dist/config-schema.d.ts +7 -7
  15. package/dist/config-schema.js +3 -3
  16. package/dist/create/board-spec.d.ts +122 -122
  17. package/dist/create/debug-artifacts.d.ts +20 -0
  18. package/dist/create/debug-artifacts.js +69 -0
  19. package/dist/create/eslint-rules-template.js +6 -3
  20. package/dist/create/index.d.ts +2 -0
  21. package/dist/create/index.js +1 -0
  22. package/dist/create/init-scaffold.d.ts +1 -0
  23. package/dist/create/init-scaffold.js +5 -0
  24. package/dist/create/init-templates.js +0 -2
  25. package/dist/emit/compliance/rules.js +52 -4
  26. package/dist/emit/emitters/function-emitter-impl.js +7 -1
  27. package/dist/emit/emitters/line-appender.js +6 -0
  28. package/dist/emit/emitters/setup.js +15 -2
  29. package/dist/emit/emitters/ui-emitter.js +40 -15
  30. package/dist/emit/route-hal-op.js +55 -1
  31. package/dist/emit/statement-renderer.js +5 -2
  32. package/dist/ir/build-ir.js +22 -1
  33. package/dist/ir/expression-to-ir.js +17 -0
  34. package/dist/ir/feature-registry.js +22 -6
  35. package/dist/ir/hal/hal-emitter.js +23 -5
  36. package/dist/ir/hal/hal-plugins.js +11 -0
  37. package/dist/ir/pin-mode-validation.js +32 -9
  38. package/dist/ir/pin-state-tracking.d.ts +58 -0
  39. package/dist/ir/pin-state-tracking.js +182 -0
  40. package/dist/ir/program-analysis.d.ts +10 -2
  41. package/dist/ir/program-analysis.js +40 -4
  42. package/dist/ir/statement-to-ir.js +14 -0
  43. package/dist/ir/transformers/control-flow.js +29 -0
  44. package/dist/ir/transformers/ui-call-resolver.js +105 -1
  45. package/dist/ir/ui-element-auto-wire.js +7 -4
  46. package/dist/orchestrator/graph-builder.d.ts +4 -1
  47. package/dist/orchestrator/graph-builder.js +7 -1
  48. package/dist/platform/async-runtime.d.ts +1 -1
  49. package/dist/platform/async-runtime.js +12 -3
  50. package/dist/platform/generic-strategy.js +1 -1
  51. package/dist/preview/api-shared-shim.d.ts +1 -0
  52. package/dist/preview/api-shared-shim.js +7 -0
  53. package/dist/preview/client.js +220 -1
  54. package/dist/preview/server.js +154 -62
  55. package/dist/theme-tokens.d.ts +22 -0
  56. package/dist/theme-tokens.js +172 -0
  57. package/dist/transpile.js +90 -12
  58. package/dist/types.d.ts +5 -0
  59. package/dist/ui-hook.d.ts +7 -0
  60. package/dist/utils/cli.js +9 -0
  61. package/dist/utils/fs.d.ts +2 -0
  62. package/dist/utils/fs.js +16 -0
  63. package/dist/utils/ui.d.ts +5 -0
  64. package/dist/utils/ui.js +7 -0
  65. package/package.json +7 -5
@@ -43,6 +43,40 @@ export const RULES = [
43
43
  justification: "AVR freeHeap() measures the gap between the stack and heap via the avr-libc __heap_start/__brkval linker symbols; pointer-to-integer conversion is the only way to compute a byte distance between two addresses on a target with no numeric free-heap API.",
44
44
  kind: "polyfill",
45
45
  },
46
+ {
47
+ // framework-zephyr BLE lowering: characteristic read handlers are TS
48
+ // functions with heterogeneous inferred return types (const char*,
49
+ // double), but the GATT attribute table registers one C callback for
50
+ // every characteristic. Handlers are stored in a fixed-size void*
51
+ // table (__tc_ble.on_read[]) and the characteristic index rides in
52
+ // bt_gatt_attr::user_data; the dispatcher casts back to the concrete
53
+ // signature per the char's type field. C++14 has no type-safe way to
54
+ // store or invoke heterogeneous signatures through one type-erased
55
+ // slot. Marker-based: every site references the table or its index
56
+ // channel.
57
+ detect: /__tc_ble\.on_read|__tc_ble_attrs|attr->user_data/,
58
+ justification: "framework-zephyr BLE lowering stores heterogeneously-typed read handlers in a void* table (__tc_ble.on_read[]) keyed by bt_gatt_attr::user_data; C++14 offers no type-safe mechanism to store or invoke mixed signatures through one type-erased slot, so the store/dispatch sites must reinterpret_cast.",
59
+ kind: "polyfill",
60
+ },
61
+ {
62
+ // Zephyr C APIs take byte buffers as uint8_t*/void* (bt_data.data,
63
+ // mqtt_utf8.utf8, MQTT payload.data, spi_buf.buf, i2c_read tx/rx)
64
+ // while cuttlefish lowers TS buffers and strings to char*/uint8_t
65
+ // arrays. The pointer reinterpretation happens exactly at the C API
66
+ // boundary in the framework shims (ble/mqtt/i2c/spi lowerings).
67
+ detect: /reinterpret_cast<(?:const )?uint8_t\*>|\.buf = reinterpret_cast<void\*>/,
68
+ justification: "Zephyr's C APIs (bt_data, mqtt_utf8, MQTT payload, spi_buf, i2c_read) accept byte buffers as uint8_t*/void* while the transpiler lowers TS buffers/strings to char*/uint8_t arrays; reinterpret_cast at the API boundary is the only way to pass the lowered buffer.",
69
+ kind: "polyfill",
70
+ },
71
+ {
72
+ // Raw register declarations (framework-native and MCU register
73
+ // exports) map a literal address to a volatile uint32_t* MMIO
74
+ // pointer. C++14 has no standard integer→pointer conversion other
75
+ // than reinterpret_cast.
76
+ detect: /reinterpret_cast<volatile uint32_t\*>/,
77
+ justification: "MMIO register declarations convert a literal address to a volatile uint32_t* pointer; reinterpret_cast is the only standard C++14 integer-to-pointer conversion.",
78
+ kind: "other",
79
+ },
46
80
  ],
47
81
  },
48
82
  { id: "M5-2-8", title: "No pointer arithmetic out of bounds", severity: "required", category: "C", enabled: true },
@@ -112,8 +146,8 @@ export const RULES = [
112
146
  },
113
147
  ],
114
148
  },
115
- { id: "A18-5-10", title: "No malloc/calloc/realloc", severity: "required", category: "C",
116
- 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,
117
151
  knownPatterns: [
118
152
  {
119
153
  // Offscreen canvas allocation (CuttlefishCanvas16/CuttlefishCanvasMono
@@ -129,10 +163,24 @@ export const RULES = [
129
163
  // the alternative (operator new) is the very thing that crashes. The
130
164
  // object is placement-constructed on the malloc'd memory and freed via
131
165
  // an explicit dtor + free, so the vtable/lifetime are correct.
132
- detect: /malloc\s*\(/,
133
- 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.",
134
169
  kind: "ts-literal",
135
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
+ },
136
184
  ],
137
185
  },
138
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)
@@ -320,6 +320,15 @@ export function buildEmitterContext(program, options) {
320
320
  if (!programAnalysis.usesNativeTiming && entryHasUI()) {
321
321
  programAnalysis.usesNativeTiming = true;
322
322
  }
323
+ // The UI runtime header calls constrain() in the progress/range node draw
324
+ // and touch-slider paths (injected by the emitter, not present in user
325
+ // source), so a mounted UI needs the constrain polyfill even when
326
+ // usesConstrain is false. OR entryHasUI() into the flag the native
327
+ // strategy reads when gating the constrain helper, mirroring the
328
+ // usesNativeTiming handling above.
329
+ if (!programAnalysis.usesConstrain && entryHasUI()) {
330
+ programAnalysis.usesConstrain = true;
331
+ }
323
332
  options.platformContext.analysis = programAnalysis;
324
333
  if (!options.platformContext.architecture && program.boardConstants) {
325
334
  options.platformContext.architecture = program.boardConstants.get("architecture");
@@ -416,8 +425,12 @@ export function buildEmitterContext(program, options) {
416
425
  }
417
426
  // The UI runtime's per-frame tick uses millis() (injected by the emitter,
418
427
  // not authored in user source), so keep the millis() shim when a UI is
419
- // mounted even if the source-level analysis didn't flag usesMillis.
420
- if (!programAnalysis.usesMillis && !entryHasUI()) {
428
+ // mounted even if the source-level analysis didn't flag usesMillis. The
429
+ // async runtime and the setInterval/setTimeout scheduler also poll
430
+ // millis() without any user-source millis() call (Async.sleep lowers to a
431
+ // raw hal-op the timing scanners can't see) — mirror the usesNativeTiming
432
+ // derivation and keep the shim for those hidden consumers too.
433
+ if (!programAnalysis.usesMillis && !programAnalysis.hasAsync && programAnalysis.timerCallCount === 0 && !entryHasUI()) {
421
434
  shimLines = shimLines.filter(l => !l.includes('millis()'));
422
435
  }
423
436
  // Strip the nullish helper FUNCTIONS (not the CUTTLEFISH_UNDEFINED macro)
@@ -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.
@@ -112,7 +112,10 @@ add(ts.SyntaxKind.BigIntLiteral, {
112
112
  hint: "Use a number literal with an explicit fixed-width type annotation.",
113
113
  code: "TS2CPP_NO_EQUIVALENT",
114
114
  eslint: {
115
- selector: "Literal[bigInt=true]",
115
+ // The parser emits the attribute as lowercase `bigint` (a string); the
116
+ // previous [bigInt=true] form matched nothing, so the editor never
117
+ // flagged what the build-time prescan did.
118
+ selector: "Literal[bigint]",
116
119
  message: "[transpiler] BigInt literals are not supported (no C++ equivalent for embedded targets). Use a number literal with an explicit fixed-width type.",
117
120
  },
118
121
  });
@@ -222,7 +225,10 @@ const CONTEXT_LINT_RULES = [
222
225
  source: "context",
223
226
  },
224
227
  {
225
- selector: "TSTypeOperator[type='keyof']",
228
+ // The parser names the attribute `operator`, not `type` — the previous
229
+ // [type='keyof'] form matched nothing, so the editor never flagged what
230
+ // the build-time prescan did.
231
+ selector: "TSTypeOperator[operator='keyof']",
226
232
  message: "[transpiler] the keyof operator is not supported (no C++ equivalent). Use a string union or a switch over field names.",
227
233
  source: "context",
228
234
  },
@@ -317,12 +323,22 @@ const CONTEXT_LINT_RULES = [
317
323
  source: "context",
318
324
  },
319
325
  {
320
- selector: "CallExpression > MemberExpression.callee[property.name=/^(bind|call|apply)$/]",
326
+ // ui.bind is a recognized UI authoring call (intercepted by the
327
+ // transpiler's call-lowering), not Function.prototype.bind — the :not()
328
+ // guard exempts the `ui` receiver so the editor selector matches the
329
+ // build-time prescan (checkContextSensitive exempts exactly ui.bind).
330
+ // The exemption lives in the selector because generateEslintConfig()
331
+ // drops non-selector fields when rendering no-restricted-syntax. Only
332
+ // `bind` is exempted — ui.call/ui.apply are not UI APIs and the prescan
333
+ // flags them, so the editor does too.
334
+ selector: "CallExpression > MemberExpression.callee[property.name='bind']:not([object.name='ui'])",
335
+ message: "[transpiler] .bind/.call/.apply rebind `this` at call time, which has no C++ lowering (this is a fixed pointer). Call the function/method directly.",
336
+ source: "context",
337
+ },
338
+ {
339
+ selector: "CallExpression > MemberExpression.callee[property.name=/^(call|apply)$/]",
321
340
  message: "[transpiler] .bind/.call/.apply rebind `this` at call time, which has no C++ lowering (this is a fixed pointer). Call the function/method directly.",
322
341
  source: "context",
323
- // ui.bind is a recognized UI authoring call (intercepted by the
324
- // transpiler's call-lowering), not Function.prototype.bind.
325
- filter: "(node) => { const o = node.callee.object; return !(o && o.type === 'Identifier' && o.name === 'ui'); }",
326
342
  },
327
343
  {
328
344
  selector: "NewExpression[callee.name='Function']",
@@ -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({