@typecad/framework-zephyr 1.0.0-alpha.15 → 1.0.0-alpha.18

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 (132) hide show
  1. package/LICENSE +202 -21
  2. package/README.md +49 -87
  3. package/dist/as-built.d.ts +2 -2
  4. package/dist/as-built.js +2 -2
  5. package/dist/boardgen.d.ts +1 -9
  6. package/dist/boardgen.js +228 -45
  7. package/dist/chips/types.d.ts +1 -1
  8. package/dist/debug-codegen.js +1 -1
  9. package/dist/display/touch-adapter.js +1 -1
  10. package/dist/display/ui-adapter.js +549 -549
  11. package/dist/doctor.js +4 -4
  12. package/dist/dt-config/custom-board.js +2 -2
  13. package/dist/dt-config/kconfig.js +18 -12
  14. package/dist/dt-config/overlay.d.ts +2 -2
  15. package/dist/dt-config/overlay.js +2 -2
  16. package/dist/framework.manifest.d.ts +3 -3
  17. package/dist/framework.manifest.js +10 -7
  18. package/dist/index.js +5 -5
  19. package/dist/licenses.d.ts +2 -2
  20. package/dist/licenses.js +8 -8
  21. package/dist/lowering/fs.js +1 -1
  22. package/dist/lowering/gpio.js +0 -32
  23. package/dist/lowering/http.js +264 -32
  24. package/dist/lowering/i2c.js +0 -8
  25. package/dist/lowering/interrupts.js +6 -0
  26. package/dist/lowering/mqtt.js +110 -8
  27. package/dist/lowering/usb.js +11 -11
  28. package/dist/lowering/wdt.js +2 -29
  29. package/dist/sdk/board-catalog-sync.d.ts +1 -3
  30. package/dist/sdk/board-catalog-sync.js +4 -10
  31. package/dist/strategy.d.ts +22 -44
  32. package/dist/strategy.js +152 -155
  33. package/dist/tmp-probe.d.ts +2 -0
  34. package/dist/tmp-probe.js +9 -0
  35. package/dist/toolchain/debug-config.d.ts +50 -90
  36. package/dist/toolchain/debug-config.js +241 -502
  37. package/dist/toolchain/env-check.d.ts +1 -3
  38. package/dist/toolchain/env-check.js +2 -7
  39. package/dist/toolchain/index.d.ts +65 -2
  40. package/dist/toolchain/index.js +432 -55
  41. package/dist/toolchain/runners.d.ts +16 -0
  42. package/dist/toolchain/runners.js +75 -0
  43. package/dist/toolchain/scaffold.d.ts +1 -1
  44. package/dist/toolchain/scaffold.js +12 -12
  45. package/dist/toolchain/west-discover.d.ts +6 -0
  46. package/dist/toolchain/west-discover.js +36 -13
  47. package/dist/toolchain/west-spawn.js +8 -2
  48. package/installer/README.md +328 -328
  49. package/installer/install.sh +2 -2
  50. package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
  51. package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
  52. package/installer/templates/project/.vscode/settings.json +1 -1
  53. package/installer/templates/project/README.md +2 -2
  54. package/package.json +5 -5
  55. package/src/as-built.ts +206 -206
  56. package/src/boardgen.ts +214 -48
  57. package/src/chips/types.ts +567 -567
  58. package/src/display/touch-adapter.ts +204 -204
  59. package/src/display/ui-adapter.ts +781 -781
  60. package/src/doctor.ts +4 -4
  61. package/src/dt-config/custom-board.ts +2 -2
  62. package/src/dt-config/kconfig.ts +18 -12
  63. package/src/dt-config/overlay.ts +1058 -1058
  64. package/src/framework.manifest.ts +538 -535
  65. package/src/index.ts +5 -5
  66. package/src/licenses.ts +425 -425
  67. package/src/lowering/fs.ts +135 -135
  68. package/src/lowering/gpio.ts +0 -33
  69. package/src/lowering/http.ts +264 -32
  70. package/src/lowering/i2c.ts +0 -9
  71. package/src/lowering/interrupts.ts +6 -0
  72. package/src/lowering/mqtt.ts +109 -8
  73. package/src/lowering/usb.ts +221 -221
  74. package/src/lowering/wdt.ts +2 -25
  75. package/src/sdk/board-catalog-sync.ts +4 -25
  76. package/src/strategy.ts +2265 -2309
  77. package/src/toolchain/debug-config.ts +264 -514
  78. package/src/toolchain/env-check.ts +279 -285
  79. package/src/toolchain/index.ts +1703 -1286
  80. package/src/toolchain/runners.ts +80 -0
  81. package/src/toolchain/scaffold.ts +296 -296
  82. package/src/toolchain/west-discover.ts +35 -13
  83. package/src/toolchain/west-spawn.ts +174 -168
  84. package/dist/async/timer-polyfill.d.ts +0 -10
  85. package/dist/async/timer-polyfill.js +0 -95
  86. package/dist/chips/board-overrides.d.ts +0 -7
  87. package/dist/chips/board-overrides.js +0 -11
  88. package/dist/chips/esp32.d.ts +0 -2
  89. package/dist/chips/esp32.js +0 -71
  90. package/dist/chips/esp32s3.d.ts +0 -2
  91. package/dist/chips/esp32s3.js +0 -103
  92. package/dist/chips/soc/.d.ts +0 -2
  93. package/dist/chips/soc/.js +0 -129
  94. package/dist/chips/soc/esp32.d.ts +0 -2
  95. package/dist/chips/soc/esp32.js +0 -120
  96. package/dist/chips/soc/esp32c3.d.ts +0 -2
  97. package/dist/chips/soc/esp32c3.js +0 -90
  98. package/dist/chips/soc/esp32c6.d.ts +0 -2
  99. package/dist/chips/soc/esp32c6.js +0 -109
  100. package/dist/chips/soc/esp32s3.d.ts +0 -2
  101. package/dist/chips/soc/esp32s3.js +0 -189
  102. package/dist/chips/soc/index.d.ts +0 -2
  103. package/dist/chips/soc/index.js +0 -23
  104. package/dist/chips/soc/nrf52840.d.ts +0 -2
  105. package/dist/chips/soc/nrf52840.js +0 -130
  106. package/dist/chips/soc/rp2040.d.ts +0 -2
  107. package/dist/chips/soc/rp2040.js +0 -141
  108. package/dist/chips/soc/rp2350a.d.ts +0 -2
  109. package/dist/chips/soc/rp2350a.js +0 -145
  110. package/dist/chips/soc/samd21g18a.d.ts +0 -2
  111. package/dist/chips/soc/samd21g18a.js +0 -143
  112. package/dist/chips/soc/stm32f411xe.d.ts +0 -2
  113. package/dist/chips/soc/stm32f411xe.js +0 -251
  114. package/dist/chips/xiao-ble.d.ts +0 -2
  115. package/dist/chips/xiao-ble.js +0 -100
  116. package/dist/lowering/power.d.ts +0 -9
  117. package/dist/lowering/power.js +0 -60
  118. package/dist/lowering/pulse.d.ts +0 -7
  119. package/dist/lowering/pulse.js +0 -51
  120. package/dist/lowering/tone.d.ts +0 -10
  121. package/dist/lowering/tone.js +0 -63
  122. package/dist/lowering/worker-backing.d.ts +0 -14
  123. package/dist/lowering/worker-backing.js +0 -79
  124. package/dist/lowering/worker.d.ts +0 -6
  125. package/dist/lowering/worker.js +0 -14
  126. package/dist/sdk/board-data.generated.d.ts +0 -2
  127. package/dist/sdk/board-data.generated.js +0 -4
  128. package/dist/sdk/catalog-walker.d.ts +0 -90
  129. package/dist/sdk/catalog-walker.js +0 -682
  130. package/dist/sdk/dts-reader.d.ts +0 -83
  131. package/dist/sdk/dts-reader.js +0 -596
  132. package/src/debug-codegen.ts +0 -207
package/src/strategy.ts CHANGED
@@ -1,2309 +1,2265 @@
1
- // ---------------------------------------------------------------------------
2
- // ZephyrStrategy — Zephyr RTOS target
3
- //
4
- // Outputs C++ built with `west` (the Zephyr build tool). Zephyr is a standard
5
- // C main()-based RTOS, so the program's entrypoint IS main(): top-level
6
- // statements lower straight into it. Worker programs keep the scaffolded
7
- // top-level `while (true)`; event-driven programs (UI / async / native
8
- // timers) end with a scheduler loop in main() that ticks the UI runtime and
9
- // pumps microtasks each frame, yielding to the kernel with k_msleep(1).
10
- //
11
- // GPIO is lowered through devicetree specs (gpio_pin_*_dt) so an active-low
12
- // LED's polarity is honored by the DT flags, not by the generated code. See
13
- // src/lowering/gpio.ts.
14
- //
15
- // EMIT BOUNDARY: This file is a canonical entry point of the framework strategy
16
- // surface (B) — its main()/shim bytes land in user firmware. The emitted bytes
17
- // are covered by the TypeCAD Runtime Exception (see RUNTIME_EXCEPTION.md at the
18
- // repository root) and are not subject to the license of this tool source.
19
- // ---------------------------------------------------------------------------
20
-
21
- import type {
22
- PlatformStrategy,
23
- ExpressionIR,
24
- ProgramIR,
25
- StatementIR,
26
- Diagnostic,
27
- PlatformContext,
28
- BoardConstants,
29
- RuntimePolyfillIR,
30
- StdLibSupport,
31
- AsyncRuntimeConfig,
32
- GraphicsCapacity,
33
- HALOpIR,
34
- DisplayHALOp,
35
- ResolvedDisplay,
36
- DisplayAdapterCode,
37
- DisplayProfile,
38
- TouchProfile,
39
- TouchAdapterCodegen,
40
- } from '@typecad/cuttlefish/api/shared';
41
- import { DEFAULT_STDLIB_SUPPORT } from '@typecad/cuttlefish/api/shared';
42
- import { applyStringMethodRewrites } from '@typecad/cuttlefish/api/shared';
43
- import { programUsesSafety } from '@typecad/cuttlefish/api';
44
- import { entryHasUI } from '@typecad/cuttlefish/ui-hook';
45
- import { generateBoard, generateBoardModuleFromContract } from './boardgen.js';
46
- import {
47
- syncBoardCatalog,
48
- ensureFreshBoardCatalog,
49
- type BoardCatalogSyncReport,
50
- type BoardCatalogEnsureResult,
51
- } from './sdk/board-catalog-sync.js';
52
- import { setActiveChip, getActiveChip, NO_BOARD_CHIP } from './chips/index.js';
53
- import { resolveChipFromBoard } from './chips/resolve.js';
54
- import { emitGpioDevDispatcher } from './chips/controllers.js';
55
- import type { ZephyrChipDescriptor } from './chips/types.js';
56
-
57
- /**
58
- * Deep-walk the program IR and collect the HAL pin numbers the program
59
- * actually touches for a peripheral family ('adc' | 'pwm') — the same walk
60
- * profileDiagnostics does. Emit paths gate per-channel state on these sets
61
- * so nothing unused reaches the single generated TU (-Wunused-function
62
- * hygiene: every emitted function/variable is referenced). `undefined`
63
- * (no program — probe paths) means "no information": callers emit every
64
- * descriptor channel, preserving probe behavior.
65
- */
66
- function collectUsedPins(
67
- program: ProgramIR | undefined,
68
- kind: 'adc' | 'pwm',
69
- chip?: ZephyrChipDescriptor,
70
- ): Set<number> | undefined {
71
- if (!program) return undefined;
72
- const pins = new Set<number>();
73
- const visit = (node: unknown): void => {
74
- if (!node || typeof node !== 'object') return;
75
- const n = node as Record<string, unknown>;
76
- const op = n.operation;
77
- if (op && typeof op === 'object') {
78
- const o = op as Record<string, unknown>;
79
- const name = o.operation;
80
- const pin = o.pin;
81
- if (typeof name === 'string' && typeof pin === 'number') {
82
- if (kind === 'adc' && (name === 'adc.read' || name === 'adc.read_voltage' || name === 'adc.read_raw' || name === 'adc.read_mv')) pins.add(pin);
83
- if (kind === 'pwm' && name.startsWith('pwm.')) pins.add(pin);
84
- }
85
- }
86
- for (const v of Object.values(n)) {
87
- if (Array.isArray(v)) { for (const item of v) visit(item); }
88
- else if (v && typeof v === 'object') visit(v);
89
- }
90
- };
91
- visit(program);
92
- return pins;
93
- }
94
-
95
- /**
96
- * The inline-override escape hatch: ADC ops carrying construction-time
97
- * device overrides (hal/adc-pin.ts opts) name DT device labels the manifest
98
- * may not know — the shim's init block must declare their device handles
99
- * (the reads reference them) and fire even when the chip has no adc facts
100
- * at all.
101
- */
102
- function collectAdcOverrideDevices(program: ProgramIR | undefined): Set<string> {
103
- const devices = new Set<string>();
104
- if (!program) return devices;
105
- const visit = (node: unknown): void => {
106
- if (!node || typeof node !== 'object') return;
107
- const n = node as Record<string, unknown>;
108
- const op = n.operation;
109
- if (op && typeof op === 'object') {
110
- const o = op as Record<string, unknown>;
111
- if (typeof o.operation === 'string' && o.operation.startsWith('adc.')
112
- && typeof o.deviceOverride === 'string' && o.deviceOverride !== '') {
113
- devices.add(o.deviceOverride);
114
- }
115
- }
116
- for (const v of Object.values(n)) {
117
- if (Array.isArray(v)) { for (const item of v) visit(item); }
118
- else if (v && typeof v === 'object') visit(v);
119
- }
120
- };
121
- visit(program);
122
- return devices;
123
- }
124
-
125
- /**
126
- * PWM construction-time overrides (the escape hatch): controller+channel
127
- * vouched for by the user on pins the manifest may not map. The shim emits
128
- * their alias vars; the overlay regen's marker merge creates the DT nodes.
129
- */
130
- function collectPwmOverrideSpecs(program: ProgramIR | undefined): { pin: number; controller: string; channel: number }[] {
131
- const specs: { pin: number; controller: string; channel: number }[] = [];
132
- if (!program) return specs;
133
- const seen = new Set<number>();
134
- const visit = (node: unknown): void => {
135
- if (!node || typeof node !== 'object') return;
136
- const n = node as Record<string, unknown>;
137
- const op = n.operation;
138
- if (op && typeof op === 'object') {
139
- const o = op as Record<string, unknown>;
140
- const ctrl = typeof o.controllerOverride === 'string' && o.controllerOverride !== '' ? o.controllerOverride : undefined;
141
- const ch = typeof o.channelOverride === 'number' && o.channelOverride >= 0 ? o.channelOverride : undefined;
142
- if (typeof o.operation === 'string' && o.operation.startsWith('pwm.')
143
- && typeof o.pin === 'number' && (ctrl || ch !== undefined) && !seen.has(o.pin)) {
144
- seen.add(o.pin);
145
- specs.push({ pin: o.pin, controller: ctrl ?? 'pwm0', channel: ch ?? 0 });
146
- }
147
- }
148
- for (const v of Object.values(n)) {
149
- if (Array.isArray(v)) { for (const item of v) visit(item); }
150
- else if (v && typeof v === 'object') visit(v);
151
- }
152
- };
153
- visit(program);
154
- return specs;
155
- }
156
-
157
- /**
158
- * Which I2C/SPI/UART controller indexes the program actually drives (parsed
159
- * from the ops' bus/port id, same as the lowerings' parseControllerIndex).
160
- * `undefined` (no program — probe paths) means "no information": callers
161
- * emit every declared controller, preserving probe behavior.
162
- *
163
- * The per-controller state blocks are emitted only for these indexes — an
164
- * unused declared controller's `__tc_<bus>N_dev` would trip -Werror's
165
- * -Wunused-variable (Zephyr builds with warnings-as-errors), the same
166
- * hygiene collectUsedPins enforces for per-channel ADC/PWM state.
167
- */
168
- function collectUsedBusIndices(program: ProgramIR | undefined): { i2c: Set<number>; spi: Set<number>; uart: Set<number> } | undefined {
169
- if (!program) return undefined;
170
- const indices = { i2c: new Set<number>(), spi: new Set<number>(), uart: new Set<number>() };
171
- const visit = (node: unknown): void => {
172
- if (!node || typeof node !== 'object') return;
173
- const n = node as Record<string, unknown>;
174
- const op = n.operation;
175
- if (op && typeof op === 'object') {
176
- const o = op as Record<string, unknown>;
177
- const name = o.operation;
178
- if (typeof name === 'string') {
179
- const m = name.match(/^(i2c|spi|uart)\./);
180
- if (m) {
181
- const bus = m[1] as 'i2c' | 'spi' | 'uart';
182
- const id = typeof o.bus === 'string' ? o.bus : typeof o.port === 'string' ? o.port : undefined;
183
- // parseControllerIndex() (what the lowering uses to pick the state
184
- // prefix) maps a digitless alias like 'Wire'/'SPI'/'Serial' to
185
- // controller 0 — the collector must agree, or the state block for
186
- // controller 0 is skipped and every __tc_<bus>0_* reference dangles.
187
- const idx = id?.match(/(\d+)$/);
188
- indices[bus].add(idx ? parseInt(idx[1], 10) : 0);
189
- }
190
- }
191
- }
192
- for (const v of Object.values(n)) {
193
- if (Array.isArray(v)) { for (const item of v) visit(item); }
194
- else if (v && typeof v === 'object') visit(v);
195
- }
196
- };
197
- visit(program);
198
- return indices;
199
- }
200
-
201
- /**
202
- * Collect the distinct DT-bound sensors the program's ops reference, as
203
- * `${part}|${bus}|${address}` keys (same shape as peripheral-usage's
204
- * sensorPartsUsed). Feeds the per-sensor device-handle state block — only
205
- * constructed sensors emit state, and only sensors whose ops were emitted
206
- * get a DT node.
207
- */
208
- export function collectSensors(program: ProgramIR | undefined): Map<string, { part: string; bus: string; port: number | string; busKind: string; spiHz: number | string; spiMode: number | string; alertPin: number | string }> | undefined {
209
- if (!program) return undefined;
210
- const sensors = new Map<string, { part: string; bus: string; port: number | string; busKind: string; spiHz: number | string; spiMode: number | string; alertPin: number | string }>();
211
- const visit = (node: unknown): void => {
212
- if (!node || typeof node !== 'object') return;
213
- const n = node as Record<string, unknown>;
214
- const op = n.operation;
215
- if (op && typeof op === 'object') {
216
- const o = op as Record<string, unknown>;
217
- if (o.operation === 'sensor.fetch' || o.operation === 'sensor.get') {
218
- const part = String(o.part ?? '');
219
- const bus = String(o.bus ?? '');
220
- const port = (o.port as number | string) ?? 0;
221
- const busKind = String(o.busKind ?? 'i2c');
222
- const spiHz = (o.spiHz as number | string) ?? 0;
223
- const spiMode = (o.spiMode as number | string) ?? 0;
224
- const alertPin = (o.alertPin as number | string) ?? -1;
225
- const key = `${part}|${bus}|${port}|${busKind}`;
226
- if (!sensors.has(key)) sensors.set(key, { part, bus, port, busKind, spiHz, spiMode, alertPin });
227
- }
228
- }
229
- for (const v of Object.values(n)) {
230
- if (Array.isArray(v)) { for (const item of v) visit(item); }
231
- else if (v && typeof v === 'object') visit(v);
232
- }
233
- };
234
- visit(program);
235
- return sensors;
236
- }
237
-
238
- /**
239
- * Collect the distinct thin SPI targets the program's ops reference
240
- * (spi.transceive / spi.dev_write), keyed `${bus}|${cs}`. Mirrors
241
- * collectSensors: only constructed targets whose ops were emitted get a
242
- * spi_dt_spec state block and a DT child node.
243
- */
244
- export function collectSpiTargets(program: ProgramIR | undefined): Map<string, { bus: string; cs: number; hz: number; mode: number }> | undefined {
245
- if (!program) return undefined;
246
- const targets = new Map<string, { bus: string; cs: number; hz: number; mode: number }>();
247
- const visit = (node: unknown): void => {
248
- if (!node || typeof node !== 'object') return;
249
- const n = node as Record<string, unknown>;
250
- const op = n.operation;
251
- if (op && typeof op === 'object') {
252
- const o = op as Record<string, unknown>;
253
- if (o.operation === 'spi.transceive' || o.operation === 'spi.dev_write' || o.operation === 'spi.reg_read') {
254
- const bus = String(o.bus ?? '');
255
- const cs = (o.cs as number) ?? 0;
256
- const key = `${bus}|${cs}`;
257
- if (!targets.has(key)) {
258
- targets.set(key, { bus, cs, hz: (o.hz as number) ?? 0, mode: (o.mode as number) ?? 0 });
259
- }
260
- }
261
- }
262
- for (const v of Object.values(n)) {
263
- if (Array.isArray(v)) { for (const item of v) visit(item); }
264
- else if (v && typeof v === 'object') visit(v);
265
- }
266
- };
267
- visit(program);
268
- return targets;
269
- }
270
-
271
- /**
272
- * Collect the distinct UART RX rings the program reads (uart.rx_* ops),
273
- * keyed by controller index. Mirrors collectSpiTargets: only ports whose RX
274
- * ops were emitted get a ring + ISR (an unused ISR is -Wunused-function
275
- * under Zephyr's -Werror).
276
- */
277
- export function collectUartRings(program: ProgramIR | undefined): Map<number, { index: number; ring: number }> | undefined {
278
- if (!program) return undefined;
279
- const rings = new Map<number, { index: number; ring: number }>();
280
- const visit = (node: unknown): void => {
281
- if (!node || typeof node !== 'object') return;
282
- const n = node as Record<string, unknown>;
283
- const op = n.operation;
284
- if (op && typeof op === 'object') {
285
- const o = op as Record<string, unknown>;
286
- if (o.operation === 'uart.rx_arm' || o.operation === 'uart.rx_available'
287
- || o.operation === 'uart.rx_peek' || o.operation === 'uart.rx_read') {
288
- const port = String(o.port ?? 'UART0');
289
- const m = port.match(/(UART|uart)(\d+)/) ?? port.match(/(\d+)/);
290
- const index = m ? parseInt(m[m.length - 1], 10) : 0;
291
- if (!rings.has(index)) {
292
- rings.set(index, { index, ring: (o.ring as number) ?? 64 });
293
- }
294
- }
295
- }
296
- for (const v of Object.values(n)) {
297
- if (Array.isArray(v)) { for (const item of v) visit(item); }
298
- else if (v && typeof v === 'object') visit(v);
299
- }
300
- };
301
- visit(program);
302
- return rings;
303
- }
304
-
305
- /**
306
- * Collect the distinct threads the program STARTS (thread.start ops), keyed
307
- * by instance. Only started threads emit state — see shimLines.
308
- */
309
- export function collectThreads(program: ProgramIR | undefined): Map<number, { instance: number; stackBytes: number; priority: number }> | undefined {
310
- if (!program) return undefined;
311
- const threads = new Map<number, { instance: number; stackBytes: number; priority: number }>();
312
- const visit = (node: unknown): void => {
313
- if (!node || typeof node !== 'object') return;
314
- const n = node as Record<string, unknown>;
315
- const op = n.operation;
316
- if (op && typeof op === 'object') {
317
- const o = op as Record<string, unknown>;
318
- if (o.operation === 'thread.start') {
319
- const instance = (o.instance as number) ?? 0;
320
- if (!threads.has(instance)) {
321
- threads.set(instance, { instance, stackBytes: (o.stackBytes as number) ?? 2048, priority: (o.priority as number) ?? 5 });
322
- }
323
- }
324
- }
325
- for (const v of Object.values(n)) {
326
- if (Array.isArray(v)) { for (const item of v) visit(item); }
327
- else if (v && typeof v === 'object') visit(v);
328
- }
329
- };
330
- visit(program);
331
- return threads;
332
- }
333
- import { lowerHalOp } from './lowering/index.js';
334
- import { adcInitLines, adcChannelForPin } from './lowering/adc.js';
335
- import { pwmInitLines } from './lowering/pwm.js';
336
- import { dacInitLines } from './lowering/dac.js';
337
- import { fsInitLines } from './lowering/fs.js';
338
- import { hwtimerInitLines } from './lowering/hwtimer.js';
339
- import { i2cInitLines } from './lowering/i2c.js';
340
- import { sensorStateLines } from './lowering/sensor.js';
341
- import { spiTargetStateLines } from './lowering/spi.js';
342
- import { threadStateLines } from './lowering/thread.js';
343
- import { uartRingStateLines } from './lowering/uart.js';
344
- import { spiInitLines } from './lowering/spi.js';
345
- import { uartInitLines } from './lowering/uart.js';
346
- import { usbInitLines, usbdDeviceLines } from './lowering/usb.js';
347
-
348
- /**
349
- * STM32F4 boot-time DBGMCU setup: set DBG_SLEEP|DBG_STOP|DBG_STANDBY
350
- * (DBGMCU_CR @ 0xE0042004, bits 0–2) so SWD stays attachable while the app
351
- * sleeps. RCC_APB1ENR (0x40023840) bit 18 clocks the DBGMCU first — F4 gates
352
- * register access behind it. Raw-register form (not the STM32 LL headers) so
353
- * the shim stays include-light; AUTOSAR-clean via reinterpret_cast.
354
- */
355
- function stm32f4DbgmcuLines(): string[] {
356
- return [
357
- '// CUTTLEFISH_STM32_DBGMCU_BEGIN',
358
- '#include <zephyr/init.h>',
359
- 'static int __tc_stm32_dbgmcu_keep_swd_alive(void) {',
360
- ' volatile uint32_t* const rcc_apb1enr = reinterpret_cast<volatile uint32_t*>(0x40023840);',
361
- ' *rcc_apb1enr = *rcc_apb1enr | (1UL << 18);',
362
- ' volatile uint32_t* const dbgmcu_cr = reinterpret_cast<volatile uint32_t*>(0xE0042004);',
363
- ' *dbgmcu_cr = *dbgmcu_cr | 0x7u;',
364
- ' return 0;',
365
- '}',
366
- 'SYS_INIT(__tc_stm32_dbgmcu_keep_swd_alive, PRE_KERNEL_1, 0);',
367
- '// CUTTLEFISH_STM32_DBGMCU_END',
368
- ];
369
- }
370
- import { interruptInitLines, collectInterruptPins } from './lowering/interrupts.js';
371
- import { wdtInitLines } from './lowering/wdt.js';
372
- import { bleInitLines } from './lowering/ble.js';
373
- import { wifiInitLines } from './lowering/wifi.js';
374
- import { httpInitLines } from './lowering/http.js';
375
- import { mqttInitLines } from './lowering/mqtt.js';
376
- import { preferencesInitLines } from './lowering/preferences.js';
377
- import { randomInitLines } from './lowering/random.js';
378
- import { generateZephyrInitCode, generateZephyrBreakpointCode, generateZephyrLogpointCode } from './debug-codegen.js';
379
- import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
380
- import { resolveZephyrDisplayOp, newDisplayState, type DisplayState } from './display/index.js';
381
- import { buildDisplayRuntime } from './display/gfx.js';
382
- import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES } from './display/profiles.js';
383
- import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
384
- import { zephyrTouchAdapter } from './display/touch-adapter.js';
385
-
386
- export class ZephyrStrategy implements PlatformStrategy {
387
- readonly id = 'zephyr';
388
-
389
- // ── Profile resolution ──────────────────────────────────────────────────
390
-
391
- /**
392
- * Resolve + cache the active chip from the platform context. Called lazily
393
- * by the methods that need the descriptor (shimLines, resolveHALOperation
394
- * via lowerHalOp).
395
- *
396
- * The chip descriptor derives from the board/MCU manifest's zephyr
397
- * fields (via boardConstants). There is no registry fallback — a board
398
- * that never generated a manifest stays NO_BOARD_CHIP.
399
- */
400
- /**
401
- * Eagerly resolve + cache the chip for `program`. The emitter calls this
402
- * before rendering any bodies — hal-op lowering reads the module-global
403
- * chip cache, and the lazy resolve (inside shimLines) runs later than the
404
- * first lowered statement.
405
- */
406
- prepareChip(program: ProgramIR | undefined, ctx?: PlatformContext): void {
407
- this.resolveChip(ctx, program);
408
- }
409
-
410
- /**
411
- * Contract-board generation: a custom PCB spec (typecad contract) names
412
- * its wired pads and bus families; the SoC's bus controller labels come
413
- * from the installed Zephyr tree's soc dtsi. Same builder as every board.
414
- */
415
- generateContractBoardModule(opts: {
416
- soc: string;
417
- zephyrBase: string;
418
- pinNames: readonly string[];
419
- padAliases?: readonly { exportName: string; padName: string }[];
420
- peripherals: { i2c: boolean; spi: boolean; uart: boolean };
421
- }): { boardTs: string; boardJson: string } {
422
- return generateBoardModuleFromContract(opts);
423
- }
424
-
425
- private resolveChip(ctx?: PlatformContext, program?: ProgramIR) {
426
- // The chip view reconstructs from the project's generated board
427
- // manifest (boardgen's zephyr.* constants) — the one path, for every
428
- // board. No curated registry, no target-based fallback: a program with
429
- // no board data resolves NO_BOARD_CHIP and the lowering reports
430
- // unsupported per subsystem.
431
- const fromBoard = resolveChipFromBoard(program?.boardConstants);
432
- setActiveChip(fromBoard ?? NO_BOARD_CHIP);
433
- return fromBoard ?? NO_BOARD_CHIP;
434
- }
435
-
436
- /**
437
- * Resolve the debug mode for the active target from the platform context.
438
- * Mirrors resolveChip's target extraction so shimLines/forcedIncludes can
439
- * gate the printf halt shim + console UART include to printf builds only
440
- * (gdb builds use VS Code native breakpoints + #line markers, so the
441
- * __tc_debug_wait_for_continue shim and its <zephyr/drivers/uart.h> include
442
- * are dead code there).
443
- */
444
- private resolveDebugMode(ctx?: PlatformContext): 'gdb' | 'printf' {
445
- const fd = ctx?.frameworkData as Record<string, unknown> | undefined;
446
- const target =
447
- (fd?.target as string | undefined) ??
448
- (fd?.buildTarget as string | undefined);
449
- return this.debugMode(target);
450
- }
451
-
452
- forcedIncludes(_program?: ProgramIR, ctx?: PlatformContext): string[] {
453
- const isPrintf = this.resolveDebugMode(ctx) === 'printf';
454
- // <zephyr/kernel.h> for k_msleep / k_uptime_get_32 / k_busy_wait / printk.
455
- // <zephyr/drivers/gpio.h> for the gpio_pin_*_dt / gpio_dt_spec API.
456
- // <cstdint> because DIRECT_CPP_TYPE_MAP passes int32_t/uint8_t through
457
- // verbatim and Zephyr's minimal C++ lib provides it.
458
- //
459
- // Driver headers are usage-gated on ctx.analysis.usesX (same flags
460
- // shimLines uses to emit the per-peripheral bus state), so an unused
461
- // peripheral doesn't pull in its header. When analysis is absent (e.g. a
462
- // capability query before a real build), the uses() helper defaults to
463
- // true so nothing is stripped — mirrors framework-esp32's forcedIncludes.
464
- const a = (ctx as any)?.analysis;
465
- const uses = (f: string): boolean => (a ? !!a[f] : true);
466
- const chip = this.resolveChip(ctx, _program);
467
- // <zephyr/drivers/gpio.h> and <cstdint> stay unconditional: gpio.h is
468
- // cross-cutting (gpio/power/interrupt/spi/pulse lowerings + the DT-spec
469
- // machinery all reference its API, and no single usesX flag owns it), and
470
- // the fixed-width types come via <zephyr/kernel.h> regardless — DIRECT_CPP_TYPE_MAP
471
- // passes int32_t/uint8_t through verbatim.
472
- const inc: string[] = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdint>'];
473
- // <cstdio> backs the printf family only: __tc_print/__tc_println (emitted
474
- // solely when @typecad/expect's preprocessor injected them — tracked via
475
- // usedPolyfillHelpers), raw printf/snprintf in user code (usesCstdio), and
476
- // the fs/preferences/uart shims (their lowerings snprintf into buffers).
477
- // A program touching none of those needs no <cstdio>.
478
- const helpers = (a as { usedPolyfillHelpers?: Set<string> } | undefined)?.usedPolyfillHelpers;
479
- const needsCstdio = uses('usesCstdio') || uses('usesFS') || uses('usesPreferences')
480
- || uses('usesUart') || uses('usesUsb')
481
- || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println');
482
- if (needsCstdio) inc.push('<cstdio>');
483
- if (uses('usesI2C')) inc.push('<zephyr/drivers/i2c.h>');
484
- if (uses('usesSensor')) inc.push('<zephyr/drivers/sensor.h>');
485
- if (uses('usesSPI')) inc.push('<zephyr/drivers/spi.h>');
486
- if (uses('usesUart')) inc.push('<zephyr/drivers/uart.h>');
487
- // uart.h is also needed by the printf-mode debug halt shim
488
- // (__tc_debug_wait_for_continue polls the console UART) even when the
489
- // program itself does not use the UART HAL. In gdb mode the shim is not
490
- // emitted, so skip the include there to avoid pulling in an unused header.
491
- if (isPrintf && !inc.includes('<zephyr/drivers/uart.h>')) inc.push('<zephyr/drivers/uart.h>');
492
- // USB CDC serial: the class instance is a UART device (uart.h); the
493
- // device context macros + usbd_* API live in the next-stack header.
494
- if (uses('usesUsb')) {
495
- if (!inc.includes('<zephyr/drivers/uart.h>')) inc.push('<zephyr/drivers/uart.h>');
496
- inc.push('<zephyr/usb/usbd.h>');
497
- // touch-to-reset shim callback reboots via NVIC_SystemReset()
498
- // (RAM-retaining Cortex-M reset — no CONFIG_REBOOT needed).
499
- if (chip?.usb?.touchReset) inc.push('<cmsis_core.h>');
500
- }
501
- if (uses('usesADC')) inc.push('<zephyr/drivers/adc.h>');
502
- if (uses('usesPWM')) inc.push('<zephyr/drivers/pwm.h>');
503
- if (uses('usesDAC')) inc.push('<zephyr/drivers/dac.h>');
504
- // Filesystem: littlefs on the storage partition. <cstring> backs the
505
- // shim's strlen; the storage/flash_map + fs/littlefs headers carry the
506
- // FIXED_PARTITION_ID macro + FS_LITTLEFS_DECLARE_DEFAULT_CONFIG the shim uses.
507
- if (uses('usesFS')) inc.push('<zephyr/fs/fs.h>', '<zephyr/fs/littlefs.h>', '<zephyr/storage/flash_map.h>', '<cstring>');
508
- // Hardware timers via the counter driver.
509
- if (uses('usesHwtimer')) inc.push('<zephyr/drivers/counter.h>');
510
- if (uses('usesWDT') || uses('usesWdt')) inc.push('<zephyr/drivers/watchdog.h>');
511
- // BLE: the bt_* GATT API + the flat-string headers the shim uses. <string>
512
- // is needed because a Utf8 (BleValueType.Utf8) read handler lowers to a
513
- // std::string-returning function (the string literal return type), and the
514
- // program-analysis usesStdString detector doesn't see types generated by
515
- // the BLE lowering layer — so without forcing <string> here, any BLE server
516
- // with a Utf8 characteristic fails to compile ('std::string does not name a
517
- // type'). <cstdlib>/<cstring> (not <stdlib.h>/<string.h>) back the shim's
518
- // strtol/strcmp/strncpy — the same AUTOSAR-compliant spelling the HTTP,
519
- // MQTT, and Preferences paths below already use.
520
- if (uses('usesBle')) inc.push('<cstdlib>', '<cstring>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
521
- // Display: the analyzer's usesDisplay flag (set by display.* hal-ops) drives
522
- // this include. When ctx.analysis is absent (capability query), uses()
523
- // defaults to true so a real build never strips it.
524
- if (uses('usesDisplay')) inc.push('<zephyr/drivers/display.h>');
525
- // Random: <zephyr/random/random.h> for sys_rand_get (the entropy tap that
526
- // seeds the __tc_rand_* xorshift32 PRNG). The shim block re-includes it, but
527
- // force it here too so a split-TU emit still has the symbol available.
528
- if (uses('usesRandom')) inc.push('<zephyr/random/random.h>');
529
- if (uses('usesWifi')) inc.push(
530
- '<zephyr/net/net_mgmt.h>', '<zephyr/net/wifi_mgmt.h>',
531
- '<zephyr/net/net_if.h>', '<zephyr/net/net_ip.h>',
532
- '<zephyr/net/conn_mgr_connectivity.h>', '<zephyr/net/conn_mgr_monitor.h>',
533
- );
534
- // HTTP/S client: Zephyr's http_client_req runs over a pre-connected socket,
535
- // so the shim pulls in the BSD socket + POSIX DNS surfaces alongside the
536
- // http client/parser headers. TLS sec tags need tls_credentials; <cstring>
537
- // /<cstdlib> back the shim's memcpy/strlen/new-nothrow usage (the core
538
- // shim only includes <cstdio>/<cstdint>).
539
- if (uses('usesHttp')) inc.push(
540
- '<zephyr/net/socket.h>', '<zephyr/net/http/client.h>',
541
- '<zephyr/net/http/parser.h>', '<zephyr/net/tls_credentials.h>',
542
- '<zephyr/posix/sys/socket.h>', '<cstring>', '<cstdlib>',
543
- );
544
- // MQTT client: <zephyr/net/mqtt.h> for mqtt_connect/publish/subscribe, plus
545
- // <zephyr/net/socket.h> for the zsock_* poll/getaddrinfo API the shim's poll
546
- // thread uses, and <zephyr/net/tls_credentials.h> for the mqtts:// path
547
- // (mqtt_sec_config). <cstring> backs the shim's memcpy/strlen.
548
- if (uses('usesMqtt')) inc.push(
549
- '<zephyr/net/mqtt.h>', '<zephyr/net/socket.h>',
550
- '<zephyr/net/tls_credentials.h>', '<cstring>',
551
- );
552
- // Preferences: Zephyr settings subsystem (ZMS backend) — settings_load/
553
- // settings_save_one/settings_delete + the SETTINGS_STATIC_HANDLER_DEFINE
554
- // macro. <cstring> backs the shim's memcpy/memmove/strncpy/strncmp (the
555
- // core shim only includes <cstdio>/<cstdint>). <errno.h> for ENOENT in h_get.
556
- if (uses('usesPreferences')) inc.push(
557
- '<zephyr/settings/settings.h>', '<cstring>', '<errno.h>',
558
- );
559
- // std::string — Zephyr has no umbrella header that transitively pulls in
560
- // <string> (unlike framework-arduino's <Arduino.h>), so a program that
561
- // lowers a std::string parameter/variable must request it explicitly. Uses
562
- // <string>, not <string.h>: the latter is the C flat-string header.
563
- if (uses('usesStdString')) inc.push('<string>');
564
- return inc;
565
- }
566
-
567
- symbolAliases(): Record<string, string> {
568
- return {};
569
- }
570
-
571
-
572
- /**
573
- * Detect async-runtime usage: a program needs the Promise/microtask runtime
574
- * if it declares an async function OR references an async-runtime symbol
575
- * (`__cuttlefish_async_`) — e.g. Async.sleep()/.then() called from a non-async
576
- * function. Mirrors Arduino's programUsesAsyncRuntime walk (that helper is
577
- * private to framework-arduino and not exported from cuttlefish, so we walk
578
- * here). The token appears in `raw` expr nodes and in `hal-expr`/`hal-op`
579
- * nodes whose resolved `raw` code references it.
580
- */
581
- private programUsesAsyncRuntime(program?: ProgramIR): boolean {
582
- if (!program) return false;
583
- const TOKEN = '__cuttlefish_async_';
584
- let found = false;
585
- const visit = (node: any): void => {
586
- if (found || !node || typeof node !== 'object') return;
587
- // raw expression node
588
- if (node.kind === 'raw' && typeof node.value === 'string' && node.value.includes(TOKEN)) {
589
- found = true; return;
590
- }
591
- // hal-expr / hal-op node whose operation is a raw op carrying code
592
- if (node.operation && typeof node.operation === 'object'
593
- && node.operation.operation === 'raw'
594
- && typeof node.operation.code === 'string'
595
- && node.operation.code.includes(TOKEN)) {
596
- found = true; return;
597
- }
598
- for (const v of Object.values(node)) {
599
- if (Array.isArray(v)) { for (const item of v) visit(item); }
600
- else if (v && typeof v === 'object') visit(v);
601
- }
602
- };
603
- visit(program);
604
- return found;
605
- }
606
-
607
- /** Pins referenced by gpio.* hal-ops in the program IR. lowerGpio routes a
608
- * pin to its devicetree spec by pin NUMBER, so the structured hal-op pins
609
- * are the authoritative signal for which __tc_dt_* specs are needed —
610
- * regardless of when the final call text is rendered. */
611
- private collectGpioPinUsage(program?: ProgramIR): Set<number> {
612
- const pins = new Set<number>();
613
- if (!program) return pins;
614
- const visit = (node: any): void => {
615
- if (!node || typeof node !== 'object') return;
616
- if (node.operation && typeof node.operation === 'object'
617
- && typeof node.operation.operation === 'string'
618
- && node.operation.operation.startsWith('gpio.')
619
- && typeof node.operation.pin === 'number') {
620
- pins.add(node.operation.pin);
621
- }
622
- for (const v of Object.values(node)) {
623
- if (Array.isArray(v)) { for (const item of v) visit(item); }
624
- else if (v && typeof v === 'object') visit(v);
625
- }
626
- };
627
- visit(program);
628
- return pins;
629
- }
630
-
631
- /** Run `re` (global) against every raw string in the IR — raw expression
632
- * values plus raw hal-op codes — returning capture group 1 of each match
633
- * (the full match when the regex has no group). This is how references the
634
- * text scanners must see but that never appear as IR call nodes (e.g. a
635
- * rawCpp() escape hatch naming `__tc_dt_sw0` directly) are discovered. */
636
- private collectRawMatches(program: ProgramIR | undefined, re: RegExp): Set<string> {
637
- const found = new Set<string>();
638
- if (!program) return found;
639
- const scan = (text: string): void => {
640
- for (const m of text.matchAll(re)) found.add(m[1] ?? m[0]);
641
- };
642
- const visit = (node: any): void => {
643
- if (!node || typeof node !== 'object') return;
644
- if (node.kind === 'raw' && typeof node.value === 'string') scan(node.value);
645
- if (node.operation && typeof node.operation === 'object'
646
- && node.operation.operation === 'raw' && typeof node.operation.code === 'string') {
647
- scan(node.operation.code);
648
- }
649
- for (const v of Object.values(node)) {
650
- if (Array.isArray(v)) { for (const item of v) visit(item); }
651
- else if (v && typeof v === 'object') visit(v);
652
- }
653
- };
654
- visit(program);
655
- return found;
656
- }
657
-
658
- /** Whether the wiring-compat GPIO read surface (__tc_gpio_read definition,
659
- * __tc_gpio_dev dispatcher, and the wiring_compat polyfill's digitalRead /
660
- * HIGH / LOW macros) must be emitted. Consumers: user digitalRead() calls
661
- * (usesDigitalRead), the @typecad/safety voter (calls __tc_gpio_read
662
- * directly via lowered raw text), and the UI runtime header's
663
- * unconditional digitalRead() poll (entryHasUI — build-global, so every TU
664
- * in a UI build carries the macros). With no analysis present (capability
665
- * query), default to emitting — same convention as the uses() helper. */
666
- private needsGpioReadShim(program?: ProgramIR, ctx?: PlatformContext): boolean {
667
- if (program && programUsesSafety(program)) return true;
668
- if (entryHasUI()) return true;
669
- const a = (ctx as any)?.analysis;
670
- return a ? !!a.usesDigitalRead : true;
671
- }
672
-
673
- shimLines(program?: ProgramIR, ctx?: PlatformContext): string[] {
674
- const chip = this.resolveChip(ctx, program);
675
- const isPrintf = this.resolveDebugMode(ctx) === 'printf';
676
- const a = (ctx as any)?.analysis;
677
- const uses = (f: string): boolean => (a ? !!a[f] : true);
678
- const helpers = (a as { usedPolyfillHelpers?: Set<string> } | undefined)?.usedPolyfillHelpers;
679
-
680
- // --- Core shim, gated item by item on actual use ------------------------
681
- // A minimal program (blink) uses none of these, and its output carries no
682
- // shim block at all. Everything up to the #endif composes into one guard
683
- // body; the guard itself is only stamped when the body is non-empty.
684
- const guardBody: string[] = [];
685
- // CUTTLEFISH_UNDEFINED: needed when the file references null/undefined
686
- // literals (usesNullish), emits nullish helper CALLS (usesNullishHelper),
687
- // or has async functions (the async state machine uses the macro for
688
- // default waitFor* timeouts — not visible to the nullish scanners).
689
- if (uses('usesNullish') || uses('usesNullishHelper') || uses('hasAsync')) {
690
- guardBody.push(
691
- '#ifndef CUTTLEFISH_UNDEFINED',
692
- '#define CUTTLEFISH_UNDEFINED 0',
693
- '#endif',
694
- );
695
- }
696
- // Nullish helpers: only when the file actually emits cuttlefish_nullish /
697
- // cuttlefish_exists CALLS (?? / ?. lowering). A file that only references
698
- // null/undefined literals needs just the macro above — the same
699
- // distinction the setup emitter's strip filter documents.
700
- if (uses('usesNullishHelper')) {
701
- guardBody.push(
702
- 'template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }',
703
- 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }',
704
- 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }',
705
- 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }',
706
- 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }',
707
- 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }',
708
- 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }',
709
- 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
710
- );
711
- }
712
- // The runtime clock contract: __tc_now_ms() is the monotonic ms clock
713
- // the shared runtimes (UI per-frame tick, async timers, scheduler)
714
- // and currentTimeMillis() lower onto. Emitted whenever anything could
715
- // read the clock: explicit Time calls, async, the interval/timeout
716
- // scheduler, or a mounted UI. uint32_t wraps every ~49.7 days — every
717
- // consumer compares by subtraction, so the wrap is harmless.
718
- if (uses('usesWallClock') || uses('hasAsync') || (!a || a.timerCallCount > 0)
719
- || this.programUsesAsyncRuntime(program) || entryHasUI()) {
720
- guardBody.push(
721
- 'inline uint32_t __tc_now_ms(void) { return k_uptime_get_32(); }',
722
- );
723
- }
724
- // Num fluent math (Num.abs/min/max) + the free math trio.
725
- // On Zephyr newlib none of these
726
- // names are macros, so the underscore-escape rewrite the
727
- // strategy needs is not. abs(long)/min/max deliberately overload (not
728
- // shadow) newlib's abs(int) — exact-match int calls still resolve to the
729
- // libc overload.
730
- if (uses('usesNum')) {
731
- guardBody.push(
732
- 'struct __tc_Num {',
733
- ' static long abs(long x) { return x < 0 ? -x : x; }',
734
- ' static long min(long a, long b) { return a < b ? a : b; }',
735
- ' static long max(long a, long b) { return a > b ? a : b; }',
736
- // The core emitter escapes abs/min/max member calls to a trailing
737
- // underscore (C-library collision) — provide the escaped spellings so
738
- // Num.abs_()/Num.min_()/Num.max_() resolve too.
739
- ' static long abs_(long x) { return x < 0 ? -x : x; }',
740
- ' static long min_(long a, long b) { return a < b ? a : b; }',
741
- ' static long max_(long a, long b) { return a > b ? a : b; }',
742
- '} Num;',
743
- 'inline long abs(long v) { return v < 0 ? -v : v; }',
744
- 'inline long min(long a, long b) { return a < b ? a : b; }',
745
- 'inline long max(long a, long b) { return a > b ? a : b; }',
746
- // Escaped free-function spellings — the escape applies to bare call
747
- // sites on some paths (abs_(-42)); cover both.
748
- 'inline long abs_(long v) { return v < 0 ? -v : v; }',
749
- 'inline long min_(long a, long b) { return a < b ? a : b; }',
750
- 'inline long max_(long a, long b) { return a > b ? a : b; }',
751
- );
752
- }
753
- // Random PRNG shim: the Random namespace lowers to the __tc_rand_* PRNG.
754
- // Emitted when the program actually uses the random.* ops, so a minimal
755
- // program's shim stays empty. The dispatcher + PRNG helpers must precede
756
- // the shims that call them (the guard block emits in push order, and
757
- // C++ needs the definitions before use).
758
- if (this.needsGpioReadShim(program, ctx)) {
759
- guardBody.push(...emitGpioDevDispatcher(chip));
760
- }
761
- if (uses('usesRandom')) {
762
- guardBody.push(...randomInitLines());
763
- }
764
- // PROGMEM note: only some UI runtime headers reference it.
765
- if (entryHasUI()) {
766
- guardBody.push(
767
- '#ifndef PROGMEM', '#define PROGMEM', '#endif',
768
- );
769
- }
770
- // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
771
- // for protocol output. Overloaded for string (const char*) and numeric
772
- // (double) so the same call site works for markers and test values.
773
- // The fs lowering bakes __tc_println into its error paths too, so the
774
- // overloads are emitted unconditionally — ctx.analysis can be absent or
775
- // stale on UI programs, and missing definitions would be a link error.
776
- //
777
- // The numeric form formats via INTEGER conversions only: libc float
778
- // printf is not dependable across SDKs — the 0.17.5 toolchain swapped
779
- // newlib for picolibc, whose default build silently prints NOTHING for
780
- // %g (the same trap as newlib-nano's -u _printf_float), which emptied
781
- // every [TC:EXPECT:...:value:] line. Integer %lld works in every libc
782
- // configuration, and the host parser accepts plain fixed-point.
783
- {
784
- guardBody.push(
785
- 'inline void __tc_print(const char* s) { printf("%s", s); }',
786
- [
787
- 'static inline char* __tc_fmt_num_buf(double v, char* out, size_t cap) {',
788
- ' if (v != v) { snprintf(out, cap, "nan"); return out; }',
789
- ' double a = v < 0 ? -v : v;',
790
- ' long long ip = (long long)a;',
791
- ' long long fr = (long long)((a - (double)ip) * 1000000.0 + 0.5);',
792
- ' if (fr >= 1000000LL) { ip += 1LL; fr = 0LL; }',
793
- ' int used = 0;',
794
- ' if (v < 0 && (ip != 0LL || fr != 0LL)) { out[used++] = \'-\'; out[used] = \'\\0\'; }',
795
- ' if (fr == 0LL) { snprintf(out + used, cap - (size_t)used, "%lld", ip); return out; }',
796
- ' char fbuf[8];',
797
- ' int len = snprintf(fbuf, sizeof(fbuf), "%06lld", fr);',
798
- ' while (len > 0 && fbuf[len - 1] == \'0\') { fbuf[--len] = \'\\0\'; }',
799
- ' snprintf(out + used, cap - (size_t)used, "%lld.%s", ip, fbuf);',
800
- ' return out;',
801
- '}',
802
- 'static void __tc_fmt_num(double v) { char __b[32]; printf("%s", __tc_fmt_num_buf(v, __b, sizeof(__b))); }',
803
- ].join('\n'),
804
- 'inline void __tc_print(double v) { __tc_fmt_num(v); }',
805
- 'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
806
- 'inline void __tc_println(double v) { __tc_fmt_num(v); printf("\\n"); }',
807
- );
808
- }
809
- // Serial-port write helper: writes a scalar to a UART/CDC device a byte at
810
- // a time. Overloaded on const char* (strings, snprintf buffers) and double
811
- // (numbers/booleans) so a single __tc_dev_put(dev, value) call site formats
812
- // any writable scalar — the same overload contract __tc_print uses. Emitted
813
- // when the program writes a UART or USB port (both include uart.h).
814
- if (uses('usesUart') || uses('usesUsb')) {
815
- guardBody.push(
816
- 'static inline void __tc_dev_put(const struct device* dev, const char* s) {',
817
- ' for (; *s != \'\\0\'; ++s) { uart_poll_out(dev, *s); }',
818
- '}',
819
- 'static inline void __tc_dev_put(const struct device* dev, double v) {',
820
- ' char __b[32];',
821
- ' __tc_dev_put(dev, __tc_fmt_num_buf(v, __b, sizeof(__b)));',
822
- '}',
823
- );
824
- }
825
-
826
- // Per-peripheral bus state — gated on the same ctx.analysis.usesX flags as
827
- // forcedIncludes, so an unused peripheral emits no state (and its header is
828
- // not included). Mirrors framework-esp32's shimLines espInit block. Within
829
- // a bus, only the instances the program drives get state — an unused
830
- // declared controller's __tc_<bus>N_dev trips -Wunused-variable under
831
- // Zephyr's -Werror (collectUsedBusIndices; probes with no program emit all).
832
- const usedBuses = collectUsedBusIndices(program);
833
- if (uses('usesI2C') && chip.i2c) {
834
- for (let i = 0; i < chip.i2c.controllers.length; i++) {
835
- if (usedBuses && !usedBuses.i2c.has(i)) continue;
836
- guardBody.push(...i2cInitLines(chip, i));
837
- }
838
- }
839
- if (uses('usesSPI') && chip.spi) {
840
- for (let i = 0; i < chip.spi.controllers.length; i++) {
841
- if (usedBuses && !usedBuses.spi.has(i)) continue;
842
- guardBody.push(...spiInitLines(chip, i));
843
- }
844
- }
845
- if (uses('usesUart') && chip.uart) {
846
- for (let i = 0; i < chip.uart.controllers.length; i++) {
847
- if (usedBuses && !usedBuses.uart.has(i)) continue;
848
- guardBody.push(...uartInitLines(chip, i));
849
- }
850
- // Thin UART RX rings — one interrupt-drained ring per port the program
851
- // reads (uart.rx_* ops). Emitted after the device handles it references.
852
- const rings = collectUartRings(program);
853
- if (rings) {
854
- for (const r of rings.values()) {
855
- guardBody.push(...uartRingStateLines(r.index, r.ring));
856
- }
857
- }
858
- }
859
- // DT-bound sensors — one state block per constructed part (device handle
860
- // + sensor_value scratch). The lowering's __tc_sensor_* references and the
861
- // overlay's DT child nodes derive from the same facts (lowering/sensor.ts).
862
- if (uses('usesSensor')) {
863
- const sensors = collectSensors(program);
864
- if (sensors) {
865
- for (const s of sensors.values()) {
866
- guardBody.push(...sensorStateLines(s.part, s.bus, s.port, s.busKind, s.spiHz, s.spiMode, s.alertPin));
867
- }
868
- }
869
- }
870
- // Thin SPI targets — one spi_dt_spec per constructed target, against the
871
- // DT child node the overlay emits. Same shared-facts discipline (the
872
- // tc-spit-cfg comment is the overlay scanner's channel).
873
- if (uses('usesSPI')) {
874
- const targets = collectSpiTargets(program);
875
- if (targets) {
876
- for (const t of targets.values()) {
877
- guardBody.push(...spiTargetStateLines(t.bus, t.cs, t.hz, t.mode));
878
- }
879
- }
880
- }
881
- // Thin Threads — one stack + k_thread + trampoline per started slot.
882
- // Keyed on thread.start ops only: join() without a prior start() on the
883
- // same index is a user error that surfaces as the undefined slot symbol.
884
- {
885
- const threads = collectThreads(program);
886
- if (threads) {
887
- for (const t of threads.values()) {
888
- guardBody.push(...threadStateLines(t.instance, t.stackBytes));
889
- }
890
- }
891
- }
892
- if (uses('usesUsb') && chip.usb) {
893
- guardBody.push(...usbdDeviceLines(chip));
894
- for (let i = 0; i < chip.usb.cdcInstances; i++) guardBody.push(...usbInitLines(chip, i));
895
- }
896
- // PWM init also fires (with alias vars for the override pins) when a
897
- // program drives ONLY inline-override channels on a chip with no pwm
898
- // facts — the lowered calls reference those aliases.
899
- {
900
- const pwmUserSpecs = collectPwmOverrideSpecs(program);
901
- if ((uses('usesPWM') && chip.pwm) || pwmUserSpecs.length > 0) {
902
- guardBody.push(...pwmInitLines(chip, collectUsedPins(program, 'pwm', chip), pwmUserSpecs));
903
- }
904
- }
905
- if (uses('usesDAC') && chip.dac) guardBody.push(...dacInitLines(chip));
906
- if (uses('usesHwtimer') && chip.hwtimer) guardBody.push(...hwtimerInitLines(chip));
907
- if (uses('usesInterrupts')) guardBody.push(...interruptInitLines(chip, program ? collectInterruptPins(program) : undefined));
908
- if ((uses('usesWDT') || uses('usesWdt')) && chip.wdt) guardBody.push(...wdtInitLines(chip));
909
- if (uses('usesBle')) guardBody.push(...bleInitLines());
910
- // Display runtime (rect/text renderer): the DIRECT-call display path (user
911
- // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
912
- // when the program uses display.* but is NOT a UI program — the UI display
913
- // adapter (emitted by cuttlefish's emitUIRuntime, solely under entryHasUI())
914
- // defines the same display_init symbol, so emitting both would collide.
915
- // `providesDisplayAdapter()` is a static capability (always true here) and
916
- // does NOT track whether the adapter is actually emitted for THIS build, so
917
- // the per-program UI signal (entryHasUI) is the correct gate. Without this,
918
- // a direct display.* program has no definition for display_init/
919
- // display_fill_rect/draw_rect/draw_text/flush (the gfx runtime was
920
- // previously dead code).
921
- if (uses('usesDisplay') && !entryHasUI()) {
922
- const rt = buildDisplayRuntime(this._displayState.profile);
923
- guardBody.push(...rt.stateLines);
924
- guardBody.push(rt.fontTable);
925
- guardBody.push(rt.helpers);
926
- }
927
- if (uses('usesWifi')) guardBody.push(...wifiInitLines());
928
- if (uses('usesHttp')) guardBody.push(...httpInitLines());
929
- if (uses('usesMqtt')) guardBody.push(...mqttInitLines());
930
- if (uses('usesPreferences')) guardBody.push(...preferencesInitLines());
931
- if (uses('usesFS')) guardBody.push(...fsInitLines());
932
- // The ADC init block also fires (with the override devices' handles)
933
- // when a program uses ONLY inline-override reads on a chip with no adc
934
- // facts — otherwise those reads reference undeclared device handles.
935
- {
936
- const adcOverrideDevices = collectAdcOverrideDevices(program);
937
- if ((uses('usesADC') && chip.adc) || adcOverrideDevices.size > 0) {
938
- guardBody.push(...adcInitLines(chip, collectUsedPins(program, 'adc'), adcOverrideDevices));
939
- }
940
- }
941
- // STM32F4: keep the core debug port alive across WFI sleep. The DBGMCU
942
- // gates PPB access while the core sleeps unless DBGMCU_CR DBG_SLEEP/
943
- // DBG_STOP/DBG_STANDBY are set — without them openocd cannot examine or
944
- // halt the running target ("Failed to read memory at 0xe000ed04", "AP
945
- // write error, reset will not halt"), and with no RST pad on boards like
946
- // the Black Pill the only recovery is the BOOT0 bootloader. Zephyr's
947
- // CONFIG_STM32_ENABLE_DEBUG_SLEEP_STOP sets only DBG_STOP on F4 (the
948
- // soc_config.c F1/L1 branch is the one that sets DBG_SLEEP), so the bits
949
- // are set here at boot, unconditionally for dev boards.
950
- // (soc is empty on board-resolved chips — the SoC name rides in the
951
- // qualified id's variant segment, e.g. 'blackpill_f411ce/stm32f411xe'.)
952
- if (chip.soc.startsWith('stm32f4') || /stm32f4\d*/.test(chip.id)) {
953
- guardBody.push(...stm32f4DbgmcuLines());
954
- }
955
-
956
- const lines: string[] = [];
957
- if (guardBody.length > 0) {
958
- lines.push(
959
- '// cuttlefish runtime shim. Wrapped in a single include guard so the',
960
- '// block is safe to emit into multiple headers and .cpp files within',
961
- '// one translation unit (a .cpp may #include several headers that each',
962
- '// carry the shim). The guard ensures the definitions are seen exactly',
963
- '// once per TU.',
964
- '#ifndef CUTTLEFISH_SHIM_DEFINED',
965
- '#define CUTTLEFISH_SHIM_DEFINED',
966
- ...guardBody,
967
- '#endif // CUTTLEFISH_SHIM_DEFINED',
968
- );
969
- }
970
-
971
- // Devicetree specs — one per board-defined GPIO pin, but ONLY for pins the
972
- // program actually addresses (lowerGpio routes by pin number, and the
973
- // structured gpio.* hal-op pins are visible here) plus aliases named
974
- // verbatim in raw code (rawCpp escape hatches). Emitted OUTSIDE the single
975
- // CUTTLEFISH_SHIM_DEFINED guard with a per-symbol guard: per-file pin sets
976
- // differ, and in a multi-header TU the first header's TU-wide guard would
977
- // otherwise hide the second header's specs. Without a program (capability
978
- // query), emit them all.
979
- const usedPins = this.collectGpioPinUsage(program);
980
- const dtTextRefs = this.collectRawMatches(program, /__tc_dt_([A-Za-z0-9_]+)/g);
981
- for (const spec of chip.gpio.dtSpecs) {
982
- if (program && !usedPins.has(spec.pin) && !dtTextRefs.has(spec.dtSpec)) continue;
983
- const guard = `__TC_DT_${spec.dtSpec.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_SPEC`;
984
- lines.push(
985
- `#ifndef ${guard}`,
986
- `#define ${guard}`,
987
- `static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`,
988
- `#endif // ${guard}`,
989
- );
990
- }
991
-
992
- // --- Debug-mode halt + per-breakpoint disable registry ---
993
- //
994
- // Printf mode only. In gdb mode the cuttlefish debug preprocessor is
995
- // skipped (core emits #line markers + VS Code native breakpoints instead),
996
- // so __tc_debug_wait_for_continue is never called — skip the shim and its
997
- // <zephyr/drivers/uart.h> dependency entirely (forcedIncludes mirrors this).
998
- //
999
- // The cuttlefish debug preprocessor injects __tc_debug_wait_for_continue(id)
1000
- // calls at each breakpoint; without these definitions the emitted code
1001
- // would not link.
1002
- //
1003
- // Zephyr's minimal libc has no getchar()/EOF, so the halt polls the console
1004
- // UART directly via uart_poll_in on the system console device, yielding to
1005
- // the scheduler with k_msleep between polls so an unattended breakpoint
1006
- // does not starve the system. ENTER (or any non-'s' byte) = continue;
1007
- // 's'/'S' = skip this breakpoint for the rest of the run (records the id).
1008
- if (isPrintf) {
1009
- lines.push(
1010
- '#ifndef __TC_BP_DISABLED_DEFINED',
1011
- '#define __TC_BP_DISABLED_DEFINED',
1012
- 'static bool __tc_bp_disabled[256] = {0};',
1013
- 'static inline bool __tc_bp_is_disabled(int id) { return id >= 0 && id < 256 && __tc_bp_disabled[id]; }',
1014
- // Console input: poll the UART console device. DEVICE_DT_GET(DT_CHOSEN(zephyr_console))
1015
- // resolves to the board's console (UART0 USB-CDC on the XIAO nRF52840).
1016
- // Not every board DTS declares a zephyr,console chosen (STM32MP1 M-side,
1017
- // display/carrier boards): there is nothing to print a prompt on and
1018
- // nothing to read a key from, so breakpoints auto-continue instead of
1019
- // hanging an unattended run.
1020
- '#if DT_HAS_CHOSEN(zephyr_console)',
1021
- 'static inline char __tc_debug_wait_for_continue(int id) {',
1022
- ' const struct device* __con = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));',
1023
- ' unsigned char __c = 0;',
1024
- " while (uart_poll_in(__con, &__c) != 0) {",
1025
- ' k_msleep(10);',
1026
- ' }',
1027
- " // Drain the rest of the typed line so the next breakpoint waits fresh.",
1028
- " unsigned char __peek = 0;",
1029
- " while (uart_poll_in(__con, &__peek) == 0 && __peek != '\\n') { (void)0; }",
1030
- " if ((__c == 's') || (__c == 'S')) { if (id >= 0 && id < 256) __tc_bp_disabled[id] = true; }",
1031
- ' return static_cast<char>(__c);',
1032
- '}',
1033
- '#else',
1034
- 'static inline char __tc_debug_wait_for_continue(int id) { (void)id; return static_cast<char>(0); }',
1035
- '#endif // DT_HAS_CHOSEN(zephyr_console)',
1036
- '#endif // __TC_BP_DISABLED_DEFINED',
1037
- '',
1038
- );
1039
- }
1040
-
1041
- // --- Zephyr entrypoint ---------------------------------------------------
1042
- // No bridge here: the synthesizer emits main() itself (it keys off
1043
- // entrypointFunctionName()="main" and requiresLoopFunction()=false), with
1044
- // the top-level statements inlined. Per-frame driving (ui_tick, microtask
1045
- // pumping, async task .run()) is appended to main() by the driver
1046
- // machinery via asyncDriverFunctionName()="main" + hostEventLoop() — see
1047
- // asyncLoopInjection for the scheduler loop it closes over.
1048
-
1049
- // GPIO read shim: emitted only when something actually reads a pin at
1050
- // runtime — user digitalRead() calls, the @typecad/safety voter (calls
1051
- // __tc_gpio_read directly), or the UI runtime header's digitalRead() poll
1052
- // (init-press-input.ts). A program that only writes/toggles GPIO needs
1053
- // neither the dispatcher nor the reader.
1054
- //
1055
- // The signature is `int` to match wiring_compat's forward declaration —
1056
- // a uint32_t definition alongside it would leave the declared int
1057
- // overload undefined (int wins overload resolution for small integer
1058
- // arguments).
1059
- //
1060
- // The pin is a RUNTIME value here (the UI pin-watch table and safety's
1061
- // voter pass whatever pin they were handed), so the controller cannot be
1062
- // baked in as a single DT_NODELABEL on a multi-controller SoC (ESP32-S3:
1063
- // pins 0–31 → gpio0, 32–48 → gpio1). Emit a tiny __tc_gpio_dev(pin)
1064
- // dispatcher that resolves the owning controller's device per pin;
1065
- // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
1066
- // still compile-time-resolved per branch, so it is always statically valid.
1067
- // (The dispatcher itself now lives at the top of the guard block — it must
1068
- // precede the wiring-ambient shims and the PinGroup factory there; the
1069
- // __tc_gpio_read body below is what still needs emitting here.)
1070
- if (this.needsGpioReadShim(program, ctx)) {
1071
- lines.push(
1072
- 'inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), __tc_gpio_pin(static_cast<uint32_t>(pin))); }',
1073
- );
1074
- }
1075
- // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
1076
- // lowering, so they stay gated on it.
1077
- if (program && programUsesSafety(program)) {
1078
- lines.push(
1079
- 'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), __tc_gpio_pin(pin), value); }',
1080
- '#ifndef __TC_DELAY_US_DEFINED',
1081
- '#define __TC_DELAY_US_DEFINED',
1082
- 'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }',
1083
- '#endif',
1084
- );
1085
- }
1086
-
1087
- return lines;
1088
- }
1089
-
1090
- profileDiagnostics(program?: ProgramIR, ctx?: PlatformContext): Diagnostic[] {
1091
- if (!program) return [];
1092
- const chip = this.resolveChip(ctx, program);
1093
- const a = (ctx as any)?.analysis ?? {};
1094
- const diags: Diagnostic[] = [];
1095
-
1096
- // Collect the pins the program uses for output config, ADC reads, and
1097
- // interrupt attaches — deep-walking the IR the same way framework-esp32
1098
- // does (its profileDiagnostics walks program to find gpio.set_mode /
1099
- // adc.read nodes).
1100
- const outputPins = new Set<number>();
1101
- const adcReadPins = new Set<number>();
1102
- const adcOverriddenPins = new Set<number>();
1103
- const pwmOverriddenPins = new Set<number>();
1104
- const interruptPins = new Set<number>();
1105
- const wdtOps = new Set<string>();
1106
- const dacPins = new Set<number>();
1107
- const pwmPins = new Set<number>();
1108
- const busInstances = { i2c: new Set<number>(), spi: new Set<number>(), uart: new Set<number>() };
1109
- const hwtimerInstances = new Set<number>();
1110
- let usesWifiOps = false;
1111
- let usesHttpOps = false;
1112
- let usesMqttOps = false;
1113
- const visit = (node: any): void => {
1114
- if (node && typeof node === 'object') {
1115
- if (node.operation && typeof node.operation === 'object') {
1116
- const op = node.operation;
1117
- if (op.operation === 'gpio.set_mode'
1118
- && typeof op.mode === 'string'
1119
- && op.mode.toLowerCase() === 'output'
1120
- && typeof op.pin === 'number') {
1121
- outputPins.add(op.pin);
1122
- }
1123
- if ((op.operation === 'adc.read' || op.operation === 'adc.read_voltage'
1124
- || op.operation === 'adc.read_raw' || op.operation === 'adc.read_mv')
1125
- && typeof op.pin === 'number') {
1126
- adcReadPins.add(op.pin);
1127
- // Inline routing overrides (construction opts): the user vouches
1128
- // for the pin — the unavailable-pin diagnostic does not apply.
1129
- if ((typeof op.channelOverride === 'number' && op.channelOverride >= 0)
1130
- || (typeof op.deviceOverride === 'string' && op.deviceOverride !== '')) {
1131
- adcOverriddenPins.add(op.pin);
1132
- }
1133
- }
1134
- if (op.operation === 'interrupt.attach_flags' && typeof op.pin === 'number') {
1135
- interruptPins.add(op.pin);
1136
- }
1137
- if (typeof op.operation === 'string' && op.operation.startsWith('wdt.')) {
1138
- wdtOps.add(op.operation);
1139
- }
1140
- if ((op.operation === 'dac.write' || op.operation === 'dac.write_value')
1141
- && typeof op.pin === 'number') {
1142
- dacPins.add(op.pin);
1143
- }
1144
- if ((op.operation === 'pwm.set_pulse' || op.operation === 'pwm.set_duty'
1145
- || op.operation === 'pwm.set_period')
1146
- && typeof op.pin === 'number') {
1147
- pwmPins.add(op.pin);
1148
- if ((typeof op.controllerOverride === 'string' && op.controllerOverride !== '')
1149
- || (typeof op.channelOverride === 'number' && op.channelOverride >= 0)) {
1150
- pwmOverriddenPins.add(op.pin);
1151
- }
1152
- }
1153
- // Bus instance usage: which I2C/SPI/UART controller indexes the
1154
- // program drives (the lowering resolves index N against
1155
- // chip.<bus>.controllers[N]; an undeclared index emits references
1156
- // to a state block that is never declared → link error).
1157
- if (typeof op.operation === 'string') {
1158
- const busMatch = op.operation.match(/^(i2c|spi|uart)\./);
1159
- if (busMatch) {
1160
- const bus = busMatch[1] as 'i2c' | 'spi' | 'uart';
1161
- const id = typeof op.bus === 'string' ? op.bus
1162
- : typeof op.port === 'string' ? op.port : undefined;
1163
- const m = id?.match(/(\d+)$/);
1164
- if (m) busInstances[bus].add(parseInt(m[1], 10));
1165
- }
1166
- }
1167
- if (typeof op.operation === 'string' && op.operation.startsWith('hwtimer.')) {
1168
- const inst = typeof op.instance === 'number'
1169
- ? op.instance
1170
- : parseInt(String(op.instance), 10);
1171
- if (!isNaN(inst)) hwtimerInstances.add(inst);
1172
- }
1173
- if (typeof op.operation === 'string' && op.operation.startsWith('wifi.')) {
1174
- usesWifiOps = true;
1175
- }
1176
- if (typeof op.operation === 'string' && op.operation.startsWith('http.')) {
1177
- usesHttpOps = true;
1178
- }
1179
- if (typeof op.operation === 'string' && op.operation.startsWith('mqtt.')) {
1180
- usesMqttOps = true;
1181
- }
1182
- }
1183
- for (const k of Object.keys(node)) {
1184
- const v = node[k];
1185
- if (Array.isArray(v)) v.forEach(visit);
1186
- else if (typeof v === 'object' && v !== null) visit(v);
1187
- }
1188
- }
1189
- };
1190
- visit(program);
1191
-
1192
- // ── ADC pin validity ────────────────────────────────────────────────────
1193
- // The SAADC lowering resolves the adc.read argument to a channel via the
1194
- // chip descriptor's adc.channels map (pin-first, then channel-number
1195
- // fallback for the Arduino-compat ADC.read(n) form — see
1196
- // adcChannelForPin). A number resolving to neither emits
1197
- // __tc_adc-1_setup() — an undefined symbol → link error. Flag it at
1198
- // compile time with a clear message instead of an opaque link failure.
1199
- const adcPins = new Set((chip.adc?.channels ?? []).map((c) => c.pin));
1200
- for (const pin of adcReadPins) {
1201
- if (adcOverriddenPins.has(pin)) continue;
1202
- if (adcChannelForPin(chip, pin) < 0) {
1203
- const valid = [...adcPins].sort((x, y) => x - y).join(', ');
1204
- // Cross-peripheral suggestion: the facts know what this pin IS wired
1205
- // to — a PWM-capable pin misread as analog is the classic mix-up.
1206
- const pwmOnPin = (chip.pwm?.specs ?? []).find((s) => s.pin === pin);
1207
- const mixup = pwmOnPin
1208
- ? ` GPIO ${pin} carries PWM on this board (${pwmOnPin.controller} ch ${pwmOnPin.channel}) — did you mean new PWM(${pin}, …)?`
1209
- : '';
1210
- diags.push({
1211
- severity: 'error',
1212
- code: 'zephyr-adc-pin-unavailable',
1213
- message: `GPIO ${pin} is not a SAADC channel on ${chip.id} and cannot be read with adc.read.`,
1214
- hint: valid
1215
- ? `Use an analog-capable pin. On ${chip.id} (SAADC): ${valid}.${mixup}`
1216
- : `This target has no ADC channels mapped in its chip descriptor.${mixup}`,
1217
- source: program.fileName,
1218
- });
1219
- }
1220
- }
1221
-
1222
- // ── Interrupt pin validity ──────────────────────────────────────────────
1223
- // GPIO.onInterrupt works on every REAL GPIO: pins listed in the chip
1224
- // descriptor's gpio.interruptPins wire through the DT-spec chain, any
1225
- // other in-range pin through the raw-controller chain. What cannot work
1226
- // is a pin number no declared controller range covers (e.g. 99 on a
1227
- // 34-pin SoC) — that lowering is a comment (silent no-op). Flag only
1228
- // that case.
1229
- const intPins = new Set((chip.gpio.interruptPins ?? []).map((p) => p.pin));
1230
- for (const pin of interruptPins) {
1231
- if (intPins.has(pin)) continue;
1232
- const inRange = !chip.gpioControllers || chip.gpioControllers.length === 0
1233
- || chip.gpioControllers.some((r) => pin >= r.minPin && pin <= r.maxPin);
1234
- if (!inRange) {
1235
- diags.push({
1236
- severity: 'error',
1237
- code: 'zephyr-interrupt-pin-unavailable',
1238
- message: `GPIO ${pin} does not exist on ${chip.id}; onInterrupt is a no-op.`,
1239
- hint: `Use a real GPIO on this board (controller ranges: ${chip.gpioControllers.map((r) => `${r.nodelabel} ${r.minPin}-${r.maxPin}`).join(', ')}).`,
1240
- source: program.fileName,
1241
- });
1242
- }
1243
- }
1244
-
1245
- // ── DAC pin validity ────────────────────────────────────────────────────
1246
- // dac.write resolves a HAL pin to a channel via the chip descriptor's
1247
- // dac.channels map. A pin not in that map lowers to a comment (silent
1248
- // no-op), and a chip without a `dac` entry (nRF52840, ESP32-S3) has no DAC
1249
- // at all. Flag either case so the user gets a clear message instead of a
1250
- // pin that silently does nothing.
1251
- if (dacPins.size > 0) {
1252
- const dacChannels = new Set((chip.dac?.channels ?? []).map((c) => c.pin));
1253
- for (const pin of dacPins) {
1254
- if (!dacChannels.has(pin)) {
1255
- const adcCh = (chip.adc?.channels ?? []).find((c) => c.pin === pin);
1256
- const mixup = adcCh
1257
- ? ` GPIO ${pin} is an ADC channel (${adcCh.controller ?? chip.adc?.nodeLabel ?? 'adc'} ch ${adcCh.channel}) — did you mean new ADC(${pin})?`
1258
- : '';
1259
- diags.push({
1260
- severity: 'error',
1261
- code: 'zephyr-dac-pin-unavailable',
1262
- message: `GPIO ${pin} is not a DAC channel on ${chip.id} and cannot be driven with dac.write.`,
1263
- hint: dacChannels.size > 0
1264
- ? `Use a DAC-capable pin. On ${chip.id}: ${[...dacChannels].sort((x, y) => x - y).join(', ')}.${mixup}`
1265
- : `${chip.id} has no DAC. Use an esp32_devkitc target (ESP32 DAC on GPIO25/26).${mixup}`,
1266
- source: program.fileName,
1267
- });
1268
- }
1269
- }
1270
- }
1271
-
1272
- // ── PWM pin validity ────────────────────────────────────────────────────
1273
- // pwm.write resolves a HAL pin to a DT spec via the chip descriptor's
1274
- // pwm.specs (static) or pwm.matrix (ESP32 LEDC: any matrix pin synthesizes
1275
- // a spec at build time). A pin on neither lowers to a comment — the pin
1276
- // silently never toggles. Flag it so the user knows (warning, not error:
1277
- // boards legitimately ship partial PWM coverage, e.g. only the aliased LED
1278
- // channel, and the rest of the program still works).
1279
- const pwmMatrix = chip.pwm?.matrix;
1280
- const pwmSpecPins = new Set([
1281
- ...(chip.pwm?.specs ?? []).map((s) => s.pin),
1282
- ...(pwmMatrix?.pins ?? []),
1283
- ]);
1284
- const pwmValid = [...pwmSpecPins].sort((x, y) => x - y).join(', ');
1285
- for (const pin of pwmPins) {
1286
- if (pwmOverriddenPins.has(pin)) continue;
1287
- if (!pwmSpecPins.has(pin)) {
1288
- // Cross-peripheral suggestion: an analog pin driven as PWM is the
1289
- // other classic mix-up.
1290
- const adcCh = (chip.adc?.channels ?? []).find((c) => c.pin === pin);
1291
- const mixup = adcCh
1292
- ? ` GPIO ${pin} is an ADC channel (${adcCh.controller ?? chip.adc?.nodeLabel ?? 'adc'} ch ${adcCh.channel}) — did you mean new ADC(${pin})?`
1293
- : '';
1294
- diags.push({
1295
- severity: 'warning',
1296
- code: 'zephyr-pwm-pin-unavailable',
1297
- message: pwmMatrix
1298
- ? `pwm on GPIO ${pin} lowers to a no-op: the pin is outside ${chip.id}'s PWM-capable set (USB, flash/PSRAM, strapping, and console pads are excluded), so nothing is driven.`
1299
- : `pwm on GPIO ${pin} lowers to a no-op: the pin has no PWM spec in ${chip.id}'s chip descriptor, so nothing is driven.`,
1300
- hint: pwmValid
1301
- ? pwmMatrix
1302
- ? `PWM-capable pins on ${chip.id} (first ${pwmMatrix.channelCount} driven get channels): ${pwmValid}.${mixup}`
1303
- : `PWM-capable pins on ${chip.id}: ${pwmValid}.${mixup}`
1304
- : `${chip.id} maps no PWM channels in its chip descriptor — pwm.* are no-ops on this target.${mixup}`,
1305
- source: program.fileName,
1306
- });
1307
- }
1308
- }
1309
- // ── Bus instance validity ───────────────────────────────────────────────
1310
- // The bus lowerings resolve instance N against chip.<bus>.controllers[N]
1311
- // and emit `__tc_<bus>N_dev` references; the state block is only declared
1312
- // for declared controllers, so an out-of-range instance is an undefined
1313
- // symbol at link time. Flag it here with the board's real controller set
1314
- // instead (mirrors how the ADC gate replaces an opaque link error).
1315
- const busLabels: Record<'i2c' | 'spi' | 'uart', { name: string; label: string }> = {
1316
- i2c: { name: 'I2C', label: 'Wire' },
1317
- spi: { name: 'SPI', label: 'SPI' },
1318
- uart: { name: 'UART', label: 'Serial' },
1319
- };
1320
- for (const bus of ['i2c', 'spi', 'uart'] as const) {
1321
- const controllers = chip[bus]?.controllers ?? [];
1322
- for (const inst of busInstances[bus]) {
1323
- if (inst < 0 || inst >= controllers.length) {
1324
- diags.push({
1325
- severity: 'error',
1326
- code: 'zephyr-bus-instance-unavailable',
1327
- message: `${busLabels[bus].name}${inst} is used but ${chip.id} declares only ${controllers.length} ${busLabels[bus].name} controller(s) in its Zephyr chip data — the generated code would not link.`,
1328
- hint: controllers.length > 0
1329
- ? `Use ${busLabels[bus].name}0 (the board's default-enabled controller: ${controllers.map((c) => c.nodeLabel).join(', ')}).`
1330
- : `${chip.id} declares no ${busLabels[bus].name} controllers for Zephyr.`,
1331
- source: program.fileName,
1332
- });
1333
- }
1334
- }
1335
- }
1336
-
1337
- // ── Hardware-timer instance validity ────────────────────────────────────
1338
- // hwtimer.* resolves the instance index to a counter device via the chip
1339
- // descriptor's hwtimer.controllers. A chip without that entry (or an
1340
- // out-of-range instance) lowers to a comment — flag it so the user knows
1341
- // the timer will never fire.
1342
- if (hwtimerInstances.size > 0) {
1343
- const controllerCount = chip.hwtimer?.controllers.length ?? 0;
1344
- for (const inst of hwtimerInstances) {
1345
- if (controllerCount === 0) {
1346
- diags.push({
1347
- severity: 'error',
1348
- code: 'zephyr-hwtimer-unavailable',
1349
- message: `Hardware timer instance ${inst} is used but ${chip.id} exposes no free counter device.`,
1350
- hint: `${chip.id} declares no hwtimer.controllers. Use a target with a free counter (e.g. nRF RTC1).`,
1351
- source: program.fileName,
1352
- });
1353
- } else if (inst < 0 || inst >= controllerCount) {
1354
- diags.push({
1355
- severity: 'error',
1356
- code: 'zephyr-hwtimer-instance-out-of-range',
1357
- message: `Hardware timer instance ${inst} is out of range on ${chip.id} (0..${controllerCount - 1}).`,
1358
- source: program.fileName,
1359
- });
1360
- }
1361
- }
1362
- }
1363
-
1364
- // ── Watchdog availability ───────────────────────────────────────────────
1365
- // wdt.* resolves the device via the chip descriptor's wdt.nodeLabel. A
1366
- // chip without that entry (e.g. SAM D21 — Zephyr's samd21 dtsi exposes
1367
- // no watchdog node) lowers to a comment — flag it so the user knows the
1368
- // watchdog never arms.
1369
- if (wdtOps.size > 0 && !chip.wdt) {
1370
- diags.push({
1371
- severity: 'error',
1372
- code: 'zephyr-wdt-unavailable',
1373
- message: `Watchdog ops are used but ${chip.id} exposes no watchdog device.`,
1374
- hint: `${chip.id} declares no wdt.nodeLabel (Zephyr's devicetree for this SoC has no watchdog node). Use a target with a watchdog, or drop the wdt.* calls.`,
1375
- source: program.fileName,
1376
- });
1377
- }
1378
-
1379
- // ── Radio presence: family-derived, not board-curated ───────────────────
1380
- // Espressif ESP32 variants carry a 2.4GHz WiFi radio; every other Zephyr
1381
- // family in the catalog is radioless (until an Ethernet/board-wifi fact
1382
- // exists). Derived from the soc name — or, when no board module was
1383
- // generated, from the raw build target the config carries.
1384
- const familyTarget = `${chip.soc || ''} ${(ctx?.frameworkData as Record<string, unknown> | undefined)?.target ?? ''} ${(ctx?.frameworkData as Record<string, unknown> | undefined)?.buildTarget ?? ''}`.toLowerCase();
1385
- const wifiSupported = chip.wifi?.supported
1386
- ?? familyTarget.split(/[^a-z0-9]+/).some((t) => t.startsWith('esp32'));
1387
-
1388
- // ── WiFi target validity ────────────────────────────────────────────────
1389
- // WiFi ops require a chip with a WiFi radio. The ESP32-S3 descriptor sets
1390
- // wifi.supported; the XIAO nRF52840 omits it (no radio). Flag wifi usage on
1391
- // a radioless chip so the user gets a clear "use an ESP32 target" message
1392
- // instead of an opaque link/DT failure.
1393
- if (usesWifiOps && !wifiSupported) {
1394
- diags.push({
1395
- severity: 'error',
1396
- code: 'zephyr-wifi-unavailable-on-target',
1397
- message: `WiFi ops are used but ${chip.id} has no WiFi radio.`,
1398
- hint: `Use an esp32s3_devkitc or esp32_devkitc target (Espressif ESP32 variants have a 2.4GHz WiFi radio).`,
1399
- source: program.fileName,
1400
- });
1401
- }
1402
-
1403
- // ── HTTP target validity ──────────────────────────────────────────────
1404
- // HTTP needs a network transport. On Zephyr the only networked target is
1405
- // the ESP32 (WiFi) — the nRF52840 has neither WiFi nor Ethernet wired in
1406
- // its chip descriptor, so the shim's socket/connect calls would fail at
1407
- // runtime. Flag http usage on a chip without a network radio so the user
1408
- // gets a clear "use an ESP32 target" message instead of an opaque link or
1409
- // runtime failure. (HTTP rides over WiFi here; an Ethernet target would
1410
- // set wifi.supported via a different transport flag if/when added.)
1411
- if (usesHttpOps && !wifiSupported) {
1412
- diags.push({
1413
- severity: 'error',
1414
- code: 'zephyr-http-unavailable-on-target',
1415
- message: `HTTP ops are used but ${chip.id} has no network stack available.`,
1416
- hint: `Use an esp32s3_devkitc or esp32_devkitc target (HTTP needs a network transport; the ESP32 WiFi radio provides it).`,
1417
- source: program.fileName,
1418
- });
1419
- }
1420
-
1421
- // ── MQTT target validity ─────────────────────────────────────────────
1422
- // Same constraint as HTTP: MQTT needs a network transport to reach a broker.
1423
- // Flag mqtt usage on a radioless chip so the user picks a networked target.
1424
- if (usesMqttOps && !wifiSupported) {
1425
- diags.push({
1426
- severity: 'error',
1427
- code: 'zephyr-mqtt-unavailable-on-target',
1428
- message: `MQTT ops are used but ${chip.id} has no network stack available.`,
1429
- hint: `Use an esp32s3_devkitc or esp32_devkitc target (MQTT needs a network transport; the ESP32 WiFi radio provides it).`,
1430
- source: program.fileName,
1431
- });
1432
- }
1433
-
1434
- // ── Unused-analysis: surface a hint that this is a no-analysis probe ────
1435
- // (intentionally minimal — esp32 has richer rules around strapping/RTC pins;
1436
- // nRF52840 has fewer silicon foot-guns, so the rules above are the material
1437
- // ones. Extend as constraints are identified.)
1438
-
1439
- return diags;
1440
- }
1441
-
1442
- /**
1443
- * Strip Arduino library headers that @typecad/hal's class surfaces register
1444
- * (I2CBus → <Wire.h>, SPIBus → <SPI.h>). The Zephyr
1445
- * equivalents are usage-gated forcedIncludes (<zephyr/drivers/i2c.h>, …),
1446
- * so an unstripped Wire.h reaches the west build and gcc rejects it
1447
- * ("Wire.h: No such file or directory") — this hook is why I2C/SPI class
1448
- * usage on Zephyr used to fail at compile time.
1449
- */
1450
- filterRequiredIncludes(includes: string[]): string[] {
1451
- // Arduino-core / AVR-only headers the HAL classes declare via __includes.
1452
- // On Zephyr the watchdog lowers to the Zephyr driver (<zephyr/drivers/
1453
- // watchdog.h>, forced under usesWDT) — <avr/wdt.h> does not exist there.
1454
- const arduinoOnlyHeaders = new Set(['<Wire.h>', '<SPI.h>', '<avr/wdt.h>', '<avr/io.h>', '<avr/interrupt.h>', '<avr/sleep.h>', '<avr/power.h>']);
1455
- return includes.filter((i) => !arduinoOnlyHeaders.has(i));
1456
- }
1457
-
1458
- /**
1459
- * Nothing to exclude: no shim or Zephyr core header forward-declares a user
1460
- * function extern (the old setup()/loop() bridge was the only consumer of
1461
- * this hook, excluding `loop`). The entrypoint itself (`main`) is already
1462
- * excluded from the static forward-declaration path because it equals
1463
- * entrypointFunctionName().
1464
- */
1465
- forwardDeclarationExclusions(): string[] {
1466
- return [];
1467
- }
1468
-
1469
- // ── File shape ──────────────────────────────────────────────────────────
1470
-
1471
- sourceExtension(): string {
1472
- return 'cpp';
1473
- }
1474
-
1475
- entrypointFunctionName(): string {
1476
- return 'main';
1477
- }
1478
-
1479
- requiresLoopFunction(): boolean {
1480
- return false;
1481
- }
1482
-
1483
- overrideBaseName(
1484
- originalBaseName: string,
1485
- outDirBaseName: string,
1486
- isEntryFile: boolean,
1487
- isNpmPackage: boolean,
1488
- ): string {
1489
- // npm packages are library-style — don't rename. Entry files (non-npm) take
1490
- // the out-dir name (mirrors the directory-name-must-match-entry rule).
1491
- // Everything else passes through. (The manifest's entrypoint.overrideBaseName
1492
- // field is dead — never read in src/ — so this method is the sole name source.)
1493
- if (isNpmPackage) return originalBaseName;
1494
- if (isEntryFile) return outDirBaseName;
1495
- return originalBaseName;
1496
- }
1497
-
1498
- effectiveEmitMode(requestedMode: string, _isNpmPackage: boolean): string {
1499
- // Zephyr always emits .cpp (no single-file entry equivalent to force away
1500
- // from), so this is passthrough regardless of npm/app. The 2-param shape
1501
- // interface and Arduino; behavior is identical across branches.
1502
- return requestedMode;
1503
- }
1504
-
1505
- // ── Type normalisation ──────────────────────────────────────────────────
1506
-
1507
- normalizeCppType(typeName: string): string {
1508
- if (typeName === 'auto') return 'auto';
1509
- if (typeName === 'std::string') return 'const char*';
1510
- return typeName;
1511
- }
1512
-
1513
- defaultNumericType(compliance?: { isBanned(ruleId: string): boolean }): string {
1514
- // Zephyr favors fixed-width types. Under AUTOSAR compliance (A3-9-1) emit
1515
- // int32_t; otherwise the standard 'int'.
1516
- return compliance?.isBanned('A3-9-1') ? 'int32_t' : 'int';
1517
- }
1518
-
1519
- mapReturnType(functionName: string, returnType: string): string {
1520
- if (functionName === 'main') return 'int';
1521
- return this.normalizeCppType(returnType);
1522
- }
1523
-
1524
- isStringLikeType(cppType: string): boolean {
1525
- return cppType === 'std::string' || cppType === 'const char*' || cppType === 'char*';
1526
- }
1527
-
1528
- isPointerType(cppType: string): boolean {
1529
- return cppType.endsWith('*');
1530
- }
1531
-
1532
- mapFunctionName(originalName: string): string {
1533
- if (originalName === '__cuttlefish_entrypoint__') return 'main';
1534
- return originalName;
1535
- }
1536
-
1537
- // ── Expression rendering ────────────────────────────────────────────────
1538
-
1539
- normalizeRawExpression(value: string): string {
1540
- let prev = '';
1541
- let v = value;
1542
- while (prev !== v) {
1543
- prev = v;
1544
- v = v.replace(/\bundefined\b/g, 'CUTTLEFISH_UNDEFINED');
1545
- v = v.replace(/\bnull\b/g, 'CUTTLEFISH_UNDEFINED');
1546
- }
1547
- // String-method lowering is shared across targets (see string-method-
1548
- // registry). Zephyr's strings are const char*, so the __tc_* helpers this
1549
- // rewrites to (defined by the string_methods polyfill) take const char*.
1550
- // includes/startsWith lower to inline strstr/strncmp (matching framework-
1551
- // arduino); everything else → a __tc_* helper call.
1552
- v = applyStringMethodRewrites(v, {
1553
- wrapReceiverFor: new Set(['indexOf']),
1554
- special: {
1555
- includes: (recv, args) => `(strstr(${recv}, ${args[0]}) != NULL)`,
1556
- startsWith: (recv, args) => `(strncmp(${recv}, ${args[0]}, strlen(${args[0]})) == 0)`,
1557
- },
1558
- });
1559
- return v;
1560
- }
1561
-
1562
- nullValue(): string {
1563
- return 'CUTTLEFISH_UNDEFINED';
1564
- }
1565
-
1566
- wrapStringConcat(): string | undefined {
1567
- return undefined;
1568
- }
1569
-
1570
- wrapStringObject(value: string): string {
1571
- return `std::to_string(${value})`;
1572
- }
1573
-
1574
- useSnprintfForStrings(): boolean {
1575
- return true;
1576
- }
1577
-
1578
- renameEnumMember(_enumName: string, memberName: string): string {
1579
- return memberName;
1580
- }
1581
-
1582
- private _largeEnumNames = new Set<string>();
1583
-
1584
- setLargeEnumNames(names: ReadonlySet<string>): void {
1585
- this._largeEnumNames = new Set(names);
1586
- }
1587
-
1588
- enumCastType(_enumName: string): string | undefined {
1589
- return undefined;
1590
- }
1591
-
1592
- renderBoardDefinitionAccess(
1593
- chain: string[],
1594
- boardConstants?: BoardConstants,
1595
- ): string | undefined {
1596
- // Fold Board.definition.<path> / Pins.definition.<path> into the literal
1597
- // board-constant value, mirroring framework-arduino. The renderer calls
1598
- // this (expression-renderer.ts) with the populated boardConstants from the
1599
- // loaded board/MCU package, so a known path resolves to its scalar value.
1600
- // `board.resolve` HAL ops, by contrast, are constant-folded earlier at
1601
- // IR-build time and never reach here; see lowering/board.ts.
1602
- if (chain.length < 3) return undefined;
1603
- if (chain[0] !== 'Board' && chain[0] !== 'Pins') return undefined;
1604
- if (chain[1] !== 'definition') return undefined;
1605
- if (!boardConstants) return undefined;
1606
- const path = chain.slice(2).join('.');
1607
- const val = boardConstants.get(path);
1608
- return val !== undefined ? String(val) : undefined;
1609
- }
1610
-
1611
- // ── Statement rendering ─────────────────────────────────────────────────
1612
-
1613
- promotesArrayLiteralsToStaticArray(): boolean {
1614
- // No std::vector in the minimal C++ lib — promote array literals to the
1615
- // StaticArray wrapper (the embedded/generic default).
1616
- return true;
1617
- }
1618
-
1619
- renderThrow(_valueExpr: string): string {
1620
- // Zephyr minimal config disables C++ exceptions (CONFIG_CPP_EXCEPTIONS=n).
1621
- // A throw becomes an infinite halt loop instead of a real throw.
1622
- return 'for (;;) { k_msleep(1000); }';
1623
- }
1624
-
1625
- objectFieldInitializer(): string | undefined {
1626
- return undefined;
1627
- }
1628
-
1629
- overrideClassFieldType(_fieldName: string, normalizedType: string): string {
1630
- return normalizedType;
1631
- }
1632
-
1633
- // ── Name guards ─────────────────────────────────────────────────────────
1634
-
1635
- reservedNames(): ReadonlySet<string> {
1636
- return new Set<string>();
1637
- }
1638
-
1639
- passthroughMacroNames(): ReadonlySet<string> {
1640
- return new Set<string>();
1641
- }
1642
-
1643
- apiReservedEnumNames(): ReadonlySet<string> {
1644
- return new Set<string>();
1645
- }
1646
-
1647
- apiReservedEnumGuard(): string {
1648
- return '';
1649
- }
1650
-
1651
- // ── Interrupt safety ─────────────────────────────────────────────────────
1652
- // Zephyr ISRs run above thread context: anything that sleeps (k_msleep),
1653
- // pends, or takes a driver lock is illegal there (asserted by the kernel in
1654
- // debug builds; corrupts scheduler state otherwise). The names below are the
1655
- // IR-level callees cuttlefish's interrupt-analysis pass matches (the same
1656
- // keys ArduinoStrategy uses; timing.delay/delay_microseconds hal-ops are
1657
- // mapped back to the bare names by the analyzer itself).
1658
- isrUnsafeOperations(): Map<string, { reason: string; severity: 'warning' | 'info' }> {
1659
- return new Map<string, { reason: string; severity: 'warning' | 'info' }>([
1660
- ['delay', {
1661
- reason: 'delay() lowers to k_msleep(), which sleeps the calling thread — illegal in Zephyr interrupt context (submit a k_work item or arm a k_timer instead)',
1662
- severity: 'warning',
1663
- }],
1664
- ['delayMicroseconds', {
1665
- reason: 'delayMicroseconds() busy-waits the CPU for the full delay, stalling every lower-priority interrupt and the scheduler for its duration',
1666
- severity: 'warning',
1667
- }],
1668
- ['I2C0', {
1669
- reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
1670
- severity: 'warning',
1671
- }],
1672
- ['I2C1', {
1673
- reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
1674
- severity: 'warning',
1675
- }],
1676
- ['SPI0', {
1677
- reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
1678
- severity: 'warning',
1679
- }],
1680
- ['SPI1', {
1681
- reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
1682
- severity: 'warning',
1683
- }],
1684
- ['UART0', {
1685
- reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
1686
- severity: 'info',
1687
- }],
1688
- ['UART1', {
1689
- reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
1690
- severity: 'info',
1691
- }],
1692
- ]);
1693
- }
1694
-
1695
- ambientTypeDeclarations(): string[] {
1696
- // Preferences is the only HAL surface the framework lowers that is used as
1697
- // a bare global (the HAL Preferences class is exported, but the canonical
1698
- // usage — and the hal/tests/14-preferences hardware suite — references it
1699
- // as an unqualified `Preferences.*`). Declaring it ambient lets those
1700
- // programs type-check and resolve to the preferences.* ops the lowering
1701
- // in src/lowering/preferences.ts handles (ZMS-backed settings). Mirrors
1702
- // framework-arduino's ambient Preferences declaration.
1703
- return [
1704
- "",
1705
- " // Persistent key/value store (ZMS-backed Zephyr settings — see",
1706
- " // src/lowering/preferences.ts). begin/end carry the namespace prefix;",
1707
- " // typed put/get round-trip through an in-RAM cache + settings_save_one.",
1708
- " const Preferences: {",
1709
- " begin(name: string, readOnly?: boolean): void;",
1710
- " end(): void;",
1711
- " clear(): void;",
1712
- " putInt(key: string, value: number): void;",
1713
- " getInt(key: string, defaultValue: number): number;",
1714
- " putUInt(key: string, value: number): void;",
1715
- " getUInt(key: string, defaultValue: number): number;",
1716
- " putBool(key: string, value: boolean): void;",
1717
- " getBool(key: string, defaultValue: boolean): boolean;",
1718
- " putFloat(key: string, value: number): void;",
1719
- " getFloat(key: string, defaultValue: number): number;",
1720
- " putString(key: string, value: string): void;",
1721
- " getString(key: string, defaultValue: string): string;",
1722
- " remove(key: string): void;",
1723
- " };",
1724
- ];
1725
- }
1726
-
1727
- // ── Includes ────────────────────────────────────────────────────────────
1728
-
1729
- needsIostream(): boolean {
1730
- return false;
1731
- }
1732
-
1733
- needsStdString(): boolean {
1734
- // Zephyr's minimal C++ lib has no <string>. Set false so the transpiler
1735
- // does not emit std::string-dependent code (e.g. the string-method
1736
- // polyfills). A program that needs it must enable a full STL.
1737
- return false;
1738
- }
1739
-
1740
- needsStdVector(): boolean {
1741
- // No <vector> in the minimal C++ lib.
1742
- return false;
1743
- }
1744
-
1745
- needsStdExcept(): boolean {
1746
- return false;
1747
- }
1748
-
1749
- needsStdFunction(): boolean {
1750
- // No <functional> in the minimal C++ lib.
1751
- return false;
1752
- }
1753
-
1754
- mathHeader(): string {
1755
- // <cmath>, not <math.h>: the shared lowering emits Math.<fn>() as
1756
- // std::<fn>() (expression-to-ir), and picolibc's <math.h> declares the
1757
- // C names in the global namespace only — std::round et al. fail to
1758
- // compile. Builds run with CONFIG_REQUIRES_FULL_LIBCPP (real libstdc++),
1759
- // whose <cmath> provides the std:: names.
1760
- return '<cmath>';
1761
- }
1762
-
1763
- cstringHeader(): string {
1764
- return '<cstring>';
1765
- }
1766
-
1767
- needsLargeEnumUnderlying(): boolean {
1768
- return false;
1769
- }
1770
-
1771
- // ── Struct field handling ───────────────────────────────────────────────
1772
-
1773
- renameStructField(fieldName: string): string {
1774
- return fieldName;
1775
- }
1776
-
1777
- structFieldInitializer(): string | undefined {
1778
- return undefined;
1779
- }
1780
-
1781
- // ── Async ───────────────────────────────────────────────────────────────
1782
- // Promises use the heap-free static runtime (generateStaticAsyncRuntime),
1783
- // pumped cooperatively via cuttlefish_pump_microtasks(). There is no
1784
- // __tc_timer_runtime.run() poll — periodic work is a Thread (k_thread) or a
1785
- // Counter (hardware timer), never a cooperative timer queue.
1786
-
1787
- getAsyncRuntimeConfig(): AsyncRuntimeConfig {
1788
- return {
1789
- queueCapacity: 64,
1790
- scheduler: 'microtask',
1791
- waitForPinEdge: 'stub',
1792
- hasPromiseRuntime: true,
1793
- hasTimers: false,
1794
- // Static (heap-free) runtime — no STL headers required.
1795
- requiredIncludes: [],
1796
- };
1797
- }
1798
-
1799
- /**
1800
- * Board module generation: join the Zephyr board data pack (extracted
1801
- * from the pinned tree) with the curated soc descriptors. See
1802
- * src/boardgen.ts.
1803
- */
1804
- generateBoardModule(
1805
- target: string,
1806
- opts?: { factsJson?: string; asBuiltJson?: string },
1807
- ): { boardTs: string; boardJson: string; warnings?: readonly string[] } | undefined {
1808
- try {
1809
- const g = generateBoard(target, { factsJson: opts?.factsJson, asBuiltJson: opts?.asBuiltJson });
1810
- return { boardTs: g.boardTs, boardJson: g.boardJson, ...(g.warnings ? { warnings: g.warnings } : {}) };
1811
- } catch (err) {
1812
- // A malformed facts/as-built file is the USER's error — surface it
1813
- // verbatim instead of the generic "cannot generate" below.
1814
- if (err instanceof Error && (err.message.includes('cuttlefish.facts.json') || err.message.includes('as-built.json'))) throw err;
1815
- return undefined;
1816
- }
1817
- }
1818
-
1819
- /**
1820
- * Regenerate the board catalog overlay from the user's own Zephyr tree —
1821
- * `cuttlefish board sync`. After a `west update`, this is how new/changed/
1822
- * removed boards reach projects without a cuttlefish release. See
1823
- * src/sdk/board-catalog-sync.ts.
1824
- */
1825
- syncBoardCatalog(zephyrBase?: string): BoardCatalogSyncReport {
1826
- return syncBoardCatalog(zephyrBase ? { zephyrBase } : {});
1827
- }
1828
-
1829
- /**
1830
- * Refresh the overlay only when it is stale (provenance no longer matches
1831
- * the tree) — the pre-step `cuttlefish board regen` runs so a regen after
1832
- * `west update` picks up the tree's boards automatically. Tree walk only
1833
- * happens when there is actual refreshing to do.
1834
- */
1835
- ensureFreshBoardCatalog(): BoardCatalogEnsureResult {
1836
- return ensureFreshBoardCatalog();
1837
- }
1838
-
1839
- asyncLoopInjection(taskVarNames: string[], config: AsyncRuntimeConfig): string[];
1840
- asyncLoopInjection(taskVarNames: string[], hasPromiseRuntime: boolean, hasTimers: boolean): string[];
1841
- asyncLoopInjection(
1842
- taskVarNames: string[],
1843
- configOrBool: AsyncRuntimeConfig | boolean,
1844
- _hasTimers?: boolean,
1845
- ): string[] {
1846
- const cfg =
1847
- typeof configOrBool === 'boolean'
1848
- ? { hasPromiseRuntime: configOrBool, hasTimers: _hasTimers ?? false }
1849
- : configOrBool;
1850
- // Drive every async state-machine task once per scheduler iteration. The
1851
- // task globals auto-start on their first .run() (constructor sets STATE_0,
1852
- // which runs unconditionally), so this is both the start and the per-frame
1853
- // advance. The state machine no-ops in its terminal/cyclic state, so
1854
- // unconditional .run() is correct (mirrors framework-arduino). No
1855
- // isComplete() gating.
1856
- const work: string[] = [];
1857
- if (cfg.hasPromiseRuntime) {
1858
- work.push('cuttlefish_pump_microtasks();');
1859
- }
1860
- for (const n of taskVarNames) {
1861
- work.push(`${n}.run();`);
1862
- }
1863
- // NOTE: no timer-queue poll — periodic work is a Thread (k_thread) or a
1864
- // Counter (hardware timer), never a cooperative poll.
1865
- //
1866
- // main() runs once, so the per-frame work must close over its own loop.
1867
- // Under a mounted UI the emitter's hostEventLoop() already wraps ui_tick +
1868
- // these lines in `while (...) { ...; k_msleep(1); }` — return flat lines
1869
- // there. Otherwise (async/timer programs with no UI) self-wrap the same
1870
- // scheduler loop here. For a program whose top-level `while (true)` never
1871
- // exits, the loop below it is unreachable — harmless, and identical to the
1872
- // old bridge behavior (whose for(;;) around loop() was equally dead).
1873
- if (entryHasUI()) {
1874
- return work;
1875
- }
1876
- return ['for (;;) {', ...work.map((l) => ` ${l}`), ' k_msleep(1);', '}'];
1877
- }
1878
-
1879
- asyncDriverFunctionName(): string {
1880
- return 'main';
1881
- }
1882
-
1883
- /**
1884
- * Zephyr's scheduler loop for event-driven programs (UI mounted). The
1885
- * emitter wraps the per-frame work — ui_tick plus the asyncLoopInjection
1886
- * lines — in `while (__tc_zephyr_event_loop) { ...; k_msleep(1); }`: a
1887
- * 1 ms cooperative yield to the kernel each frame, matching the cadence the
1888
- * old setup()/loop() bridge gave loop(). The flag is never cleared — the
1889
- * loop is the program's lifetime — but keeping the bool gives the condition
1890
- * a used symbol (a bare `while (true)` would leave nothing to name, and an
1891
- * unused flag would trip -Werror's -Wunused-variable).
1892
- */
1893
- hostEventLoop(): {
1894
- flagName: string;
1895
- continueCondition: string;
1896
- preIteration: string;
1897
- postIteration: string;
1898
- } | null {
1899
- return {
1900
- flagName: '__tc_zephyr_event_loop',
1901
- continueCondition: '__tc_zephyr_event_loop',
1902
- preIteration: '',
1903
- postIteration: 'k_msleep(1);',
1904
- };
1905
- }
1906
-
1907
- // ── Type aliases ────────────────────────────────────────────────────────
1908
-
1909
- shouldSkipTypeAlias(): boolean {
1910
- return false;
1911
- }
1912
-
1913
- // ── Diagnostics ─────────────────────────────────────────────────────────
1914
-
1915
- emitDiagnostics(): Diagnostic[] {
1916
- return [];
1917
- }
1918
-
1919
- currentTimeMillis(): string {
1920
- return '__tc_now_ms()';
1921
- }
1922
-
1923
- // ── Build configuration ──────────────────────────────────────────────────
1924
-
1925
- asyncQueueCapacity(): number {
1926
- return 64;
1927
- }
1928
-
1929
- outputSubdirectory(_baseName: string): string {
1930
- // Zephyr convention: application sources live under src/.
1931
- return 'src';
1932
- }
1933
-
1934
- generateHeaderFile(): boolean {
1935
- return true;
1936
- }
1937
-
1938
- enumApiGuard(_enumName: string): { open: string; close: string } | undefined {
1939
- return undefined;
1940
- }
1941
-
1942
- getStdLibSupport(_architecture?: string): StdLibSupport {
1943
- // Zephyr's minimal C++ support (lib/cpp/minimal) provides only <cstddef>,
1944
- // <cstdint>, <new>. No <vector>, <string>, <iostream>, <functional>, no
1945
- // exceptions, no RTTI. The blink MVP uses only GPIO + kernel timing, so
1946
- // none of those are needed. Array/string literals are not promoted to the
1947
- // STL containers; a future program needing them must enable a full STL
1948
- // and update these flags.
1949
- return {
1950
- hasVector: false,
1951
- hasString: false,
1952
- hasIostream: false,
1953
- hasExceptions: false,
1954
- hasRTTI: false,
1955
- recommendedArrayImpl: 'static_array',
1956
- recommendedStringImpl: 'static_string',
1957
- };
1958
- }
1959
-
1960
- // ── Polyfills ───────────────────────────────────────────────────────────
1961
- // Zephyr is a no-STL target (hasVector/hasString = false), so array/string
1962
- // literals lower to __tc_StaticArray / const char* and string methods lower
1963
- // to __tc_* helpers — both need STL-free definitions emitted here (there is
1964
- // no shared-runtime fallback; the pipeline sources 100% of polyfills from
1965
- // generateNativePolyfills). Mirrors framework-arduino's AVR polyfills.
1966
-
1967
- nativePolyfills(): Set<string> {
1968
- // cuttlefish_halt: always (the runtime header may reference it).
1969
- // string_methods / static_array: STL-free array + string helpers a no-STL
1970
- // target needs (mutated/struct array literals + any string method).
1971
- // async_runtime: heap-free static Promise/microtask runtime (no STL needed).
1972
- return new Set<string>([
1973
- 'cuttlefish_halt', 'wiring_compat', 'string_methods', 'static_array',
1974
- 'async_runtime',
1975
- ]);
1976
- }
1977
-
1978
- generateNativePolyfills(program?: ProgramIR, ctx?: PlatformContext): RuntimePolyfillIR[] {
1979
- // wiring_compat (digitalRead/HIGH/LOW macros + the __tc_gpio_read forward
1980
- // declaration) is emitted only when something reads a pin: user
1981
- // digitalRead() calls, the @typecad/safety voter, or the UI runtime
1982
- // header's unconditional digitalRead() poll (init-press-input.ts — the
1983
- // loop body is dead when no pin watchers are configured but must
1984
- // compile). needsGpioReadShim defaults to true without analysis so
1985
- // capability queries keep seeing it.
1986
- const wiringCompat: RuntimePolyfillIR = {
1987
- // Wiring-compatibility shims for symbols the UI runtime header
1988
- // references unconditionally (e.g. init-press-input.ts polls pin
1989
- // watchers via digitalRead/HIGH/LOW even when none are configured —
1990
- // the loop body is dead but must compile). Zephyr lowers GPIO through
1991
- // its __tc_gpio_* helpers (defined in shimLines); these macros route
1992
- // the Wiring tokens to them.
1993
- kind: 'polyfill',
1994
- id: 'wiring_compat',
1995
- domain: 'standard' as const,
1996
- requiredIncludes: [],
1997
- forwardDeclarations: [
1998
- // Forward-declared so the digitalRead macro (below) can reference it
1999
- // before the shim block defines the body. The shim emits the full
2000
- // definition via gpio_pin_get_raw.
2001
- 'int __tc_gpio_read(int pin);',
2002
- ],
2003
- helperStructs: [],
2004
- helperFunctions: [],
2005
- shimMacros: [
2006
- '#ifndef HIGH',
2007
- '#define HIGH 1',
2008
- '#endif',
2009
- '#ifndef LOW',
2010
- '#define LOW 0',
2011
- '#endif',
2012
- '#ifndef digitalRead',
2013
- '#define digitalRead(pin) __tc_gpio_read(pin)',
2014
- '#endif',
2015
- ],
2016
- dependencies: [],
2017
- };
2018
- const polyfills: RuntimePolyfillIR[] = [
2019
- {
2020
- kind: 'polyfill',
2021
- id: 'cuttlefish_halt',
2022
- domain: 'standard' as const,
2023
- requiredIncludes: [],
2024
- forwardDeclarations: [],
2025
- helperStructs: [],
2026
- helperFunctions: [
2027
- '[[noreturn]] inline void cuttlefish_halt() { for (;;) { k_msleep(1000); } }',
2028
- ],
2029
- shimMacros: [],
2030
- dependencies: [],
2031
- },
2032
- ...(this.needsGpioReadShim(program, ctx) ? [wiringCompat] : []),
2033
- {
2034
- // STL-free string-method polyfills. String methods (.toUpperCase(),
2035
- // .includes(), .substring(), …) lower at IR level to __tc_* helpers for
2036
- // every target; this supplies their definitions. Minimal-libc friendly:
2037
- // only <cstring> primitives (no <cctype> — case conversion is inline
2038
- // ASCII so the polyfill is self-contained). Mirrors framework-arduino.
2039
- kind: 'polyfill',
2040
- id: 'string_methods',
2041
- domain: 'embedded' as const,
2042
- requiredIncludes: ['<cstring>'],
2043
- forwardDeclarations: [],
2044
- helperStructs: [],
2045
- helperFunctions: [`
2046
- // TypeCAD string method polyfills (Zephyr, minimal-libc).
2047
- #ifndef CUTTLEFISH_STR_BUF_SIZE
2048
- #define CUTTLEFISH_STR_BUF_SIZE 64
2049
- #endif
2050
- bool __tc_endsWith(const char* s, const char* suffix) { int sl = strlen(s), tl = strlen(suffix); return sl >= tl && strcmp(s + sl - tl, suffix) == 0; }
2051
- const char* __tc_toUpperCase(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; for (char* p = b; *p; p++) { if (*p >= 'a' && *p <= 'z') { *p = static_cast<char>(*p - 32); } } return b; }
2052
- const char* __tc_toLowerCase(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; for (char* p = b; *p; p++) { if (*p >= 'A' && *p <= 'Z') { *p = static_cast<char>(*p + 32); } } return b; }
2053
- const char* __tc_trim(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; while (*s == ' ' || *s == '\\t' || *s == '\\n' || *s == '\\r') s++; int len = strlen(s); while (len > 0 && (s[len-1] == ' ' || s[len-1] == '\\t' || s[len-1] == '\\n' || s[len-1] == '\\r')) len--; int cplen = len < CUTTLEFISH_STR_BUF_SIZE - 1 ? len : CUTTLEFISH_STR_BUF_SIZE - 1; strncpy(b, s, cplen); b[cplen] = '\\0'; return b; }
2054
- const char* __tc_substring2(const char* s, int start, int end) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; int slen = strlen(s); if (start < 0) start = 0; if (end > slen) end = slen; if (end < start) end = start; int len = end - start; if (len >= CUTTLEFISH_STR_BUF_SIZE) len = CUTTLEFISH_STR_BUF_SIZE - 1; strncpy(b, s + start, len); b[len] = '\\0'; return b; }
2055
- const char* __tc_substring1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
2056
- const char* __tc_slice2(const char* s, int start, int end) { return __tc_substring2(s, start, end); }
2057
- const char* __tc_slice1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
2058
- const char* __tc_replace(const char* s, const char* old, const char* repl) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; const char* pos = strstr(s, old); if (!pos) { strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; return b; } int beforeLen = static_cast<int>(pos - s); int oldLen = static_cast<int>(strlen(old)); int replLen = static_cast<int>(strlen(repl)); if (beforeLen + replLen + static_cast<int>(strlen(pos + oldLen)) >= CUTTLEFISH_STR_BUF_SIZE) { strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; return b; } memcpy(b, s, beforeLen); memcpy(b + beforeLen, repl, replLen); strcpy(b + beforeLen + replLen, pos + oldLen); return b; }
2059
- const char* __tc_charAt(const char* s, int idx) { static char buf[2][2]; static uint8_t slot = 0; slot ^= 1; buf[slot][0] = s[idx]; buf[slot][1] = '\\0'; return buf[slot]; }
2060
- int __tc_charCodeAt(const char* s, int idx) { return static_cast<int>(static_cast<unsigned char>(s[idx])); }
2061
- int __tc_indexOf(const char* s, const char* needle) { const char* p = strstr(s, needle); return p ? static_cast<int>(p - s) : -1; }
2062
- `],
2063
- shimMacros: [],
2064
- dependencies: [],
2065
- },
2066
- {
2067
- // STL-free fixed-size array wrapper. Mutated/struct-element array
2068
- // literals and array methods (.push/.pop/.map/.filter) lower to
2069
- // __tc_StaticArray<T,N>; this supplies the template. Idempotent guard
2070
- // so a redefinition is a no-op. Mirrors framework-arduino.
2071
- kind: 'polyfill',
2072
- id: 'static_array',
2073
- domain: 'embedded' as const,
2074
- requiredIncludes: [],
2075
- forwardDeclarations: [],
2076
- helperStructs: [],
2077
- helperFunctions: [`
2078
- #ifndef __TC_STATIC_ARRAY_DEFINED
2079
- #define __TC_STATIC_ARRAY_DEFINED
2080
- template<typename T, int N>
2081
- struct __tc_StaticArray {
2082
- T data[N];
2083
- int _size;
2084
- __tc_StaticArray() : _size(0) {}
2085
- int length() const { return _size; }
2086
- int size() const { return _size; }
2087
- void push(T val) { if (_size < N) data[_size++] = val; }
2088
- T pop() { return (_size > 0) ? data[--_size] : T(); }
2089
- int indexOf(T val) const { for (int i = 0; i < _size; i++) if (data[i] == val) return i; return -1; }
2090
- T& operator[](int i) { return data[i]; }
2091
- const T& operator[](int i) const { return data[i]; }
2092
- T* begin() { return &data[0]; }
2093
- T* end() { return &data[_size]; }
2094
- const T* begin() const { return &data[0]; }
2095
- const T* end() const { return &data[_size]; }
2096
- };
2097
- #endif
2098
- `],
2099
- shimMacros: [],
2100
- dependencies: [],
2101
- },
2102
- ];
2103
-
2104
- // async_runtime — heap-free static Promise/microtask runtime. Emitted when
2105
- // the program declares an async function OR references an async-runtime
2106
- // symbol (Async.sleep/.then from a non-async fn). The static path requires
2107
- // no STL headers, so it is safe under Zephyr's minimal C++ lib.
2108
- const usesAsync = !!program
2109
- && (program.functions.some((fn: any) => fn && fn.isAsync) || this.programUsesAsyncRuntime(program));
2110
- if (usesAsync) {
2111
- polyfills.push({
2112
- kind: 'polyfill',
2113
- id: 'async_runtime',
2114
- domain: 'embedded',
2115
- requiredIncludes: [],
2116
- // Polyfill definitions emit before shimLines, but the runtime's
2117
- // timer bodies call __tc_now_ms() (defined in shimLines) — declare
2118
- // it first so the polyfill compiles even for programs whose source
2119
- // has no explicit timing call.
2120
- forwardDeclarations: ['uint32_t __tc_now_ms(void);'],
2121
- helperStructs: [generateStaticAsyncRuntime(8, this.getAsyncRuntimeConfig().waitForPinEdge)],
2122
- helperFunctions: [],
2123
- shimMacros: [],
2124
- dependencies: [],
2125
- hasPromiseRuntime: true,
2126
- } as RuntimePolyfillIR);
2127
- }
2128
- return polyfills;
2129
- }
2130
-
2131
- // ── HAL ──────────────────────────────────────────────────────────────────
2132
-
2133
- resolveHALOperation(op: HALOpIR): { code?: string; expression?: string } | undefined {
2134
- return lowerHalOp(op);
2135
- }
2136
-
2137
- modelsGpio(): boolean {
2138
- return true;
2139
- }
2140
-
2141
- // ── Atomic HAL primitives ─────────────────────────────────────────────────
2142
- // Zephyr lowers GPIO through devicetree specs and its own __tc_gpio_* helpers
2143
- // (defined in shimLines via gpio_pin_get_raw / gpio_pin_set_raw). Cuttlefish
2144
- // asks these instead of emitting Wiring tokens by name. The async polling
2145
- // path is gated to 'stub' on Zephyr (waitForPinEdge), so delayMs is unlikely
2146
- // to be called here, but a busy-wait form is provided for completeness.
2147
- readDigitalPin(pin: string): string {
2148
- return `__tc_gpio_read(${pin})`;
2149
- }
2150
- readAnalogPin(pin: string): string {
2151
- // Zephyr ADC is lowered through its own shim; this stub keeps cuttlefish
2152
- // from emitting a Wiring analogRead token. Update if a __tc_adc_read helper
2153
- // is introduced.
2154
- return `/* adc lowering via zephyr shim */ 0`;
2155
- }
2156
- writeDigitalPin(pin: string, val: string): string {
2157
- return `__tc_gpio_write(${pin}, ${val})`;
2158
- }
2159
- setPinMode(_pin: string, _mode: string): string {
2160
- // Zephyr configures pin direction via devicetree, not a runtime pinMode.
2161
- return `/* pin mode configured via devicetree */`;
2162
- }
2163
- delayMs(ms: string): string {
2164
- return `k_msleep(${ms})`;
2165
- }
2166
- delayMicroseconds(us: string): string {
2167
- return `__tc_delay_us(${us})`;
2168
- }
2169
- halCallNames(): ReadonlySet<string> {
2170
- // Zephyr's HAL surface uses __tc_ prefixed helpers + the Zephyr API.
2171
- return new Set<string>([
2172
- "__tc_gpio_read", "__tc_gpio_write", "__tc_delay_us",
2173
- "gpio_pin_get_raw", "gpio_pin_set_raw", "k_msleep", "k_busy_wait",
2174
- ]);
2175
- }
2176
- isHalCall(name: string): boolean {
2177
- return this.halCallNames().has(name);
2178
- }
2179
- analogReadCallNames(): ReadonlySet<string> {
2180
- return new Set<string>();
2181
- }
2182
-
2183
- // ── RTOS ─────────────────────────────────────────────────────────────────
2184
-
2185
- isRtosTarget(): boolean {
2186
- // Zephyr is a preemptive RTOS — delay()/k_msleep inside main() is the
2187
- // expected cooperative yield, not an anti-pattern to warn about.
2188
- return true;
2189
- }
2190
-
2191
- // Display state (mirrors Arduino's _displayCtx). Seeded on display.init; the
2192
- // validator-probe path seeds the default profile lazily.
2193
- private _displayState: DisplayState = newDisplayState();
2194
-
2195
- // ── Graphics ──────────────────────────────────────────────────────────────
2196
- // Generic <zephyr/drivers/display.h> + ported GFX primitives (see src/display/).
2197
- // resolveDisplayOp delegates to resolveZephyrDisplayOp with the per-build
2198
- // DisplayState; the GFX runtime (device handle + line buffer + helpers) is
2199
- // emitted into shimLines when usesDisplay.
2200
-
2201
- resolveDisplayOp(op: DisplayHALOp): { code?: string; expression?: string } | undefined {
2202
- return resolveZephyrDisplayOp(op, this._displayState);
2203
- }
2204
-
2205
- supportedDisplayDrivers(): ReadonlySet<string> {
2206
- return new Set<string>(Object.keys(ZEPHYR_DISPLAY_PROFILES));
2207
- }
2208
-
2209
- // ── Strategy-owned display/touch adapter seam ────────────────────────────
2210
- // Zephyr owns its display + touch adapters: the UI display adapter bridges
2211
- // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
2212
- // format, see src/display/ui-adapter.ts), and the touch adapters drive the
2213
- // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
2214
- // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
2215
- // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
2216
- // knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
2217
-
2218
- providesDisplayAdapter(): boolean { return true; }
2219
-
2220
- resolveDisplayAdapter(display: ResolvedDisplay): DisplayAdapterCode | undefined {
2221
- const code = zephyrDisplayAdapterGenerator(display);
2222
- return code ?? undefined;
2223
- }
2224
-
2225
- providesTouchAdapter(): boolean { return true; }
2226
-
2227
- resolveTouchAdapter(touch: TouchProfile): TouchAdapterCodegen | undefined {
2228
- return zephyrTouchAdapter(touch);
2229
- }
2230
-
2231
- // Named display-profile registry: maps config `profile` values (e.g.
2232
- // "st7796-zephyr") to the shared DisplayProfile shape so transpile.ts can
2233
- // resolve them per-framework. The Zephyr profiles are DT-binding descriptors;
2234
- // BUILT_IN_PROFILES (display/profiles.ts) is the single DT-binding →
2235
- // shared-shape mapping, shared with the preview's registry loader.
2236
- getProfileRegistry(): Map<string, DisplayProfile> {
2237
- return new Map(Object.entries(BUILT_IN_PROFILES));
2238
- }
2239
-
2240
- colorFormat(): 'rgb565' | 'rgb666' | 'rgb888' | 'mono' {
2241
- return 'rgb565';
2242
- }
2243
-
2244
- graphicsCapacity(): GraphicsCapacity {
2245
- return {
2246
- maxNodes: 256,
2247
- maxBindings: 64,
2248
- maxActiveTransitions: 32,
2249
- nodeStorage: 'flash',
2250
- };
2251
- }
2252
-
2253
- // ── Debug ─────────────────────────────────────────────────────────────────
2254
- // Zephyr's minimal C++ config has no <iostream>, so the GenericStrategy
2255
- // std::cout fallback the debug preprocessor uses by default would NOT
2256
- // compile. Override the debug surface to route through printk (always
2257
- // available, no CONFIG_CONSOLE dependency) and the __tc_debug_wait_for_continue
2258
- // halt emitted in shimLines. See src/debug-codegen.ts.
2259
- //
2260
- // Target-selective: targets with a debug probe get native GDB source-level
2261
- // debugging (core emits #line markers + skips printf instrumentation); the
2262
- // rest fall back to the printk instrumentation path. The ESP32-S3 has a
2263
- // built-in USB-JTAG (single-cable GDB via OpenOCD) so it selects 'gdb'.
2264
- // The XIAO nRF52840 needs its J-Link wired up; its GDB path is a follow-on,
2265
- // so it stays on printf for now.
2266
-
2267
- debugMode(target?: string): 'gdb' | 'printf' {
2268
- // `target` is the Zephyr board id (optionally with a /qualifier suffix,
2269
- // e.g. 'esp32s3_devkitc/esp32s3/procpu'). Match on the bare board id.
2270
- const boardId = (target ?? '').split('/')[0];
2271
- if (boardId === 'esp32s3_devkitc' || boardId.startsWith('esp32s3')) {
2272
- return 'gdb';
2273
- }
2274
- // The STM32 Black Pill ships a verified ST-Link probe method in its board
2275
- // package (openocd runner over SWD, with the reset_config quirk for the
2276
- // unwired SRST line), so F5 attaches natively out of the box.
2277
- if (boardId.startsWith('blackpill_')) {
2278
- return 'gdb';
2279
- }
2280
- // The plain ESP32 (esp32_devkitc) intentionally stays on 'printf': unlike
2281
- // the S3 it has NO built-in USB-JTAG, so gdb needs an external ESP-PROG
2282
- // probe + a different OpenOCD cfg/toolchain dir (deferred). Falls through.
2283
- return 'printf';
2284
- }
2285
-
2286
- generateDebugInitCode(): string[] {
2287
- return generateZephyrInitCode();
2288
- }
2289
-
2290
- generateDebugBreakpointCode(params: {
2291
- fileName: string; lineNum: number; originalLine: string;
2292
- variables: Array<{ name: string; isFunction?: boolean; cppType?: 'bool'|'int'|'long'|'float'|'string'|'unknown' }>;
2293
- normalizedCondition?: string;
2294
- breakpointId?: number;
2295
- }): string[] {
2296
- return generateZephyrBreakpointCode(
2297
- params.fileName, params.lineNum, params.originalLine,
2298
- params.variables, params.normalizedCondition, params.breakpointId,
2299
- );
2300
- }
2301
-
2302
- generateDebugLogpointCode(params: {
2303
- fileName: string; lineNum: number;
2304
- parts: Array<{ type: 'text' | 'variable'; value: string }>;
2305
- variables: Array<{ name: string; isFunction?: boolean }>;
2306
- }): string[] {
2307
- return generateZephyrLogpointCode(params.fileName, params.lineNum, params.parts, params.variables);
2308
- }
2309
- }
1
+ // ---------------------------------------------------------------------------
2
+ // ZephyrStrategy — Zephyr RTOS target
3
+ //
4
+ // Outputs C++ built with `west` (the Zephyr build tool). Zephyr is a standard
5
+ // C main()-based RTOS, so the program's entrypoint IS main(): top-level
6
+ // statements lower straight into it. Worker programs keep the scaffolded
7
+ // top-level `while (true)`; event-driven programs (UI / async / native
8
+ // timers) end with a scheduler loop in main() that ticks the UI runtime and
9
+ // pumps microtasks each frame, yielding to the kernel with k_msleep(1).
10
+ //
11
+ // GPIO is lowered through devicetree specs (gpio_pin_*_dt) so an active-low
12
+ // LED's polarity is honored by the DT flags, not by the generated code. See
13
+ // src/lowering/gpio.ts.
14
+ //
15
+ // EMIT BOUNDARY: This file is a canonical entry point of the framework strategy
16
+ // surface (B) — its main()/shim bytes land in user firmware. The emitted bytes
17
+ // are covered by the TypeCAD Runtime Exception (see RUNTIME_EXCEPTION.md at the
18
+ // repository root) and are not subject to the license of this tool source.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ import type {
22
+ PlatformStrategy,
23
+ ExpressionIR,
24
+ ProgramIR,
25
+ StatementIR,
26
+ Diagnostic,
27
+ PlatformContext,
28
+ BoardConstants,
29
+ RuntimePolyfillIR,
30
+ StdLibSupport,
31
+ AsyncRuntimeConfig,
32
+ GraphicsCapacity,
33
+ HALOpIR,
34
+ DisplayHALOp,
35
+ ResolvedDisplay,
36
+ DisplayAdapterCode,
37
+ DisplayProfile,
38
+ TouchProfile,
39
+ TouchAdapterCodegen,
40
+ } from '@typecad/cuttlefish/api/shared';
41
+ import { DEFAULT_STDLIB_SUPPORT } from '@typecad/cuttlefish/api/shared';
42
+ import { applyStringMethodRewrites } from '@typecad/cuttlefish/api/shared';
43
+ import { programUsesSafety } from '@typecad/cuttlefish/api';
44
+ import { entryHasUI } from '@typecad/cuttlefish/ui-hook';
45
+ import { generateBoard, generateBoardModuleFromContract } from './boardgen.js';
46
+ import {
47
+ syncBoardCatalog,
48
+ ensureFreshBoardCatalog,
49
+ type BoardCatalogSyncReport,
50
+ type BoardCatalogEnsureResult,
51
+ } from './sdk/board-catalog-sync.js';
52
+ import { setActiveChip, getActiveChip, NO_BOARD_CHIP } from './chips/index.js';
53
+ import { resolveChipFromBoard } from './chips/resolve.js';
54
+ import { emitGpioDevDispatcher } from './chips/controllers.js';
55
+ import type { ZephyrChipDescriptor } from './chips/types.js';
56
+
57
+ /**
58
+ * Visit the program's transpile-resolved HAL ops with the same `visit` the IR
59
+ * walk uses. A HAL call inlined into another HAL call's template literal
60
+ * (`USB0.writeLine(\`n: ${gps.available()}\`)`) is lowered to C++ text while
61
+ * the IR is built — it never becomes a hal-op/hal-expr IR node, so the walk
62
+ * alone cannot see it. Such ops ride on ProgramIR.resolvedHalOps; wrapped as
63
+ * `{ operation: op }` they match the hal-op node shape every collector below
64
+ * tests for. Without this fold, shims keyed on these scans (UART RX rings,
65
+ * sensor device handles, per-controller bus state, ADC channels/overrides,
66
+ * dt specs) go undeclared while the baked text still references them — the
67
+ * `'__tc_dt_led0' was not declared` failure class.
68
+ */
69
+ function visitResolvedHalOps(
70
+ program: ProgramIR | undefined,
71
+ visit: (node: unknown) => void,
72
+ ): void {
73
+ for (const op of program?.resolvedHalOps ?? []) visit({ operation: op });
74
+ }
75
+
76
+ /**
77
+ * Deep-walk the program IR and collect the HAL pin numbers the program
78
+ * actually touches for a peripheral family ('adc' | 'pwm') — the same walk
79
+ * profileDiagnostics does. Emit paths gate per-channel state on these sets
80
+ * so nothing unused reaches the single generated TU (-Wunused-function
81
+ * hygiene: every emitted function/variable is referenced). `undefined`
82
+ * (no program — probe paths) means "no information": callers emit every
83
+ * descriptor channel, preserving probe behavior.
84
+ */
85
+ function collectUsedPins(
86
+ program: ProgramIR | undefined,
87
+ kind: 'adc' | 'pwm',
88
+ chip?: ZephyrChipDescriptor,
89
+ ): Set<number> | undefined {
90
+ if (!program) return undefined;
91
+ const pins = new Set<number>();
92
+ const visit = (node: unknown): void => {
93
+ if (!node || typeof node !== 'object') return;
94
+ const n = node as Record<string, unknown>;
95
+ const op = n.operation;
96
+ if (op && typeof op === 'object') {
97
+ const o = op as Record<string, unknown>;
98
+ const name = o.operation;
99
+ const pin = o.pin;
100
+ if (typeof name === 'string' && typeof pin === 'number') {
101
+ if (kind === 'adc' && (name === 'adc.read' || name === 'adc.read_voltage' || name === 'adc.read_raw' || name === 'adc.read_mv')) pins.add(pin);
102
+ if (kind === 'pwm' && name.startsWith('pwm.')) pins.add(pin);
103
+ }
104
+ }
105
+ for (const v of Object.values(n)) {
106
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
107
+ else if (v && typeof v === 'object') visit(v);
108
+ }
109
+ };
110
+ visit(program);
111
+ visitResolvedHalOps(program, visit);
112
+ return pins;
113
+ }
114
+
115
+ /**
116
+ * The inline-override escape hatch: ADC ops carrying construction-time
117
+ * device overrides (hal/adc-pin.ts opts) name DT device labels the manifest
118
+ * may not know — the shim's init block must declare their device handles
119
+ * (the reads reference them) and fire even when the chip has no adc facts
120
+ * at all.
121
+ */
122
+ function collectAdcOverrideDevices(program: ProgramIR | undefined): Set<string> {
123
+ const devices = new Set<string>();
124
+ if (!program) return devices;
125
+ const visit = (node: unknown): void => {
126
+ if (!node || typeof node !== 'object') return;
127
+ const n = node as Record<string, unknown>;
128
+ const op = n.operation;
129
+ if (op && typeof op === 'object') {
130
+ const o = op as Record<string, unknown>;
131
+ if (typeof o.operation === 'string' && o.operation.startsWith('adc.')
132
+ && typeof o.deviceOverride === 'string' && o.deviceOverride !== '') {
133
+ devices.add(o.deviceOverride);
134
+ }
135
+ }
136
+ for (const v of Object.values(n)) {
137
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
138
+ else if (v && typeof v === 'object') visit(v);
139
+ }
140
+ };
141
+ visit(program);
142
+ visitResolvedHalOps(program, visit);
143
+ return devices;
144
+ }
145
+
146
+ /**
147
+ * PWM construction-time overrides (the escape hatch): controller+channel
148
+ * vouched for by the user on pins the manifest may not map. The shim emits
149
+ * their alias vars; the overlay regen's marker merge creates the DT nodes.
150
+ */
151
+ function collectPwmOverrideSpecs(program: ProgramIR | undefined): { pin: number; controller: string; channel: number }[] {
152
+ const specs: { pin: number; controller: string; channel: number }[] = [];
153
+ if (!program) return specs;
154
+ const seen = new Set<number>();
155
+ const visit = (node: unknown): void => {
156
+ if (!node || typeof node !== 'object') return;
157
+ const n = node as Record<string, unknown>;
158
+ const op = n.operation;
159
+ if (op && typeof op === 'object') {
160
+ const o = op as Record<string, unknown>;
161
+ const ctrl = typeof o.controllerOverride === 'string' && o.controllerOverride !== '' ? o.controllerOverride : undefined;
162
+ const ch = typeof o.channelOverride === 'number' && o.channelOverride >= 0 ? o.channelOverride : undefined;
163
+ if (typeof o.operation === 'string' && o.operation.startsWith('pwm.')
164
+ && typeof o.pin === 'number' && (ctrl || ch !== undefined) && !seen.has(o.pin)) {
165
+ seen.add(o.pin);
166
+ specs.push({ pin: o.pin, controller: ctrl ?? 'pwm0', channel: ch ?? 0 });
167
+ }
168
+ }
169
+ for (const v of Object.values(n)) {
170
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
171
+ else if (v && typeof v === 'object') visit(v);
172
+ }
173
+ };
174
+ visit(program);
175
+ visitResolvedHalOps(program, visit);
176
+ return specs;
177
+ }
178
+
179
+ /**
180
+ * Which I2C/SPI/UART controller indexes the program actually drives (parsed
181
+ * from the ops' bus/port id, same as the lowerings' parseControllerIndex).
182
+ * `undefined` (no program — probe paths) means "no information": callers
183
+ * emit every declared controller, preserving probe behavior.
184
+ *
185
+ * The per-controller state blocks are emitted only for these indexes — an
186
+ * unused declared controller's `__tc_<bus>N_dev` would trip -Werror's
187
+ * -Wunused-variable (Zephyr builds with warnings-as-errors), the same
188
+ * hygiene collectUsedPins enforces for per-channel ADC/PWM state.
189
+ */
190
+ function collectUsedBusIndices(program: ProgramIR | undefined): { i2c: Set<number>; spi: Set<number>; uart: Set<number> } | undefined {
191
+ if (!program) return undefined;
192
+ const indices = { i2c: new Set<number>(), spi: new Set<number>(), uart: new Set<number>() };
193
+ const visit = (node: unknown): void => {
194
+ if (!node || typeof node !== 'object') return;
195
+ const n = node as Record<string, unknown>;
196
+ const op = n.operation;
197
+ if (op && typeof op === 'object') {
198
+ const o = op as Record<string, unknown>;
199
+ const name = o.operation;
200
+ if (typeof name === 'string') {
201
+ const m = name.match(/^(i2c|spi|uart)\./);
202
+ if (m) {
203
+ const bus = m[1] as 'i2c' | 'spi' | 'uart';
204
+ const id = typeof o.bus === 'string' ? o.bus : typeof o.port === 'string' ? o.port : undefined;
205
+ // parseControllerIndex() (what the lowering uses to pick the state
206
+ // prefix) maps a digitless alias like 'Wire'/'SPI'/'Serial' to
207
+ // controller 0 — the collector must agree, or the state block for
208
+ // controller 0 is skipped and every __tc_<bus>0_* reference dangles.
209
+ const idx = id?.match(/(\d+)$/);
210
+ indices[bus].add(idx ? parseInt(idx[1], 10) : 0);
211
+ }
212
+ }
213
+ }
214
+ for (const v of Object.values(n)) {
215
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
216
+ else if (v && typeof v === 'object') visit(v);
217
+ }
218
+ };
219
+ visit(program);
220
+ visitResolvedHalOps(program, visit);
221
+ return indices;
222
+ }
223
+
224
+ /**
225
+ * Collect the distinct DT-bound sensors the program's ops reference, as
226
+ * `${part}|${bus}|${address}` keys (same shape as peripheral-usage's
227
+ * sensorPartsUsed). Feeds the per-sensor device-handle state block — only
228
+ * constructed sensors emit state, and only sensors whose ops were emitted
229
+ * get a DT node.
230
+ */
231
+ export function collectSensors(program: ProgramIR | undefined): Map<string, { part: string; bus: string; port: number | string; busKind: string; spiHz: number | string; spiMode: number | string; alertPin: number | string }> | undefined {
232
+ if (!program) return undefined;
233
+ const sensors = new Map<string, { part: string; bus: string; port: number | string; busKind: string; spiHz: number | string; spiMode: number | string; alertPin: number | string }>();
234
+ const visit = (node: unknown): void => {
235
+ if (!node || typeof node !== 'object') return;
236
+ const n = node as Record<string, unknown>;
237
+ const op = n.operation;
238
+ if (op && typeof op === 'object') {
239
+ const o = op as Record<string, unknown>;
240
+ if (o.operation === 'sensor.fetch' || o.operation === 'sensor.get') {
241
+ const part = String(o.part ?? '');
242
+ const bus = String(o.bus ?? '');
243
+ const port = (o.port as number | string) ?? 0;
244
+ const busKind = String(o.busKind ?? 'i2c');
245
+ const spiHz = (o.spiHz as number | string) ?? 0;
246
+ const spiMode = (o.spiMode as number | string) ?? 0;
247
+ const alertPin = (o.alertPin as number | string) ?? -1;
248
+ const key = `${part}|${bus}|${port}|${busKind}`;
249
+ if (!sensors.has(key)) sensors.set(key, { part, bus, port, busKind, spiHz, spiMode, alertPin });
250
+ }
251
+ }
252
+ for (const v of Object.values(n)) {
253
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
254
+ else if (v && typeof v === 'object') visit(v);
255
+ }
256
+ };
257
+ visit(program);
258
+ visitResolvedHalOps(program, visit);
259
+ return sensors;
260
+ }
261
+
262
+ /**
263
+ * Collect the distinct thin SPI targets the program's ops reference
264
+ * (spi.transceive / spi.dev_write), keyed `${bus}|${cs}`. Mirrors
265
+ * collectSensors: only constructed targets whose ops were emitted get a
266
+ * spi_dt_spec state block and a DT child node.
267
+ */
268
+ export function collectSpiTargets(program: ProgramIR | undefined): Map<string, { bus: string; cs: number; hz: number; mode: number }> | undefined {
269
+ if (!program) return undefined;
270
+ const targets = new Map<string, { bus: string; cs: number; hz: number; mode: number }>();
271
+ const visit = (node: unknown): void => {
272
+ if (!node || typeof node !== 'object') return;
273
+ const n = node as Record<string, unknown>;
274
+ const op = n.operation;
275
+ if (op && typeof op === 'object') {
276
+ const o = op as Record<string, unknown>;
277
+ if (o.operation === 'spi.transceive' || o.operation === 'spi.dev_write' || o.operation === 'spi.reg_read') {
278
+ const bus = String(o.bus ?? '');
279
+ const cs = (o.cs as number) ?? 0;
280
+ const key = `${bus}|${cs}`;
281
+ if (!targets.has(key)) {
282
+ targets.set(key, { bus, cs, hz: (o.hz as number) ?? 0, mode: (o.mode as number) ?? 0 });
283
+ }
284
+ }
285
+ }
286
+ for (const v of Object.values(n)) {
287
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
288
+ else if (v && typeof v === 'object') visit(v);
289
+ }
290
+ };
291
+ visit(program);
292
+ visitResolvedHalOps(program, visit);
293
+ return targets;
294
+ }
295
+
296
+ /**
297
+ * Collect the distinct UART RX rings the program reads (uart.rx_* ops),
298
+ * keyed by controller index. Mirrors collectSpiTargets: only ports whose RX
299
+ * ops were emitted get a ring + ISR (an unused ISR is -Wunused-function
300
+ * under Zephyr's -Werror).
301
+ */
302
+ export function collectUartRings(program: ProgramIR | undefined): Map<number, { index: number; ring: number }> | undefined {
303
+ if (!program) return undefined;
304
+ const rings = new Map<number, { index: number; ring: number }>();
305
+ const visit = (node: unknown): void => {
306
+ if (!node || typeof node !== 'object') return;
307
+ const n = node as Record<string, unknown>;
308
+ const op = n.operation;
309
+ if (op && typeof op === 'object') {
310
+ const o = op as Record<string, unknown>;
311
+ if (o.operation === 'uart.rx_arm' || o.operation === 'uart.rx_available'
312
+ || o.operation === 'uart.rx_peek' || o.operation === 'uart.rx_read') {
313
+ const port = String(o.port ?? 'UART0');
314
+ const m = port.match(/(UART|uart)(\d+)/) ?? port.match(/(\d+)/);
315
+ const index = m ? parseInt(m[m.length - 1], 10) : 0;
316
+ if (!rings.has(index)) {
317
+ rings.set(index, { index, ring: (o.ring as number) ?? 64 });
318
+ }
319
+ }
320
+ }
321
+ for (const v of Object.values(n)) {
322
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
323
+ else if (v && typeof v === 'object') visit(v);
324
+ }
325
+ };
326
+ visit(program);
327
+ visitResolvedHalOps(program, visit);
328
+ return rings;
329
+ }
330
+
331
+ /**
332
+ * Collect the distinct threads the program STARTS (thread.start ops), keyed
333
+ * by instance. Only started threads emit state — see shimLines.
334
+ */
335
+ export function collectThreads(program: ProgramIR | undefined): Map<number, { instance: number; stackBytes: number; priority: number }> | undefined {
336
+ if (!program) return undefined;
337
+ const threads = new Map<number, { instance: number; stackBytes: number; priority: number }>();
338
+ const visit = (node: unknown): void => {
339
+ if (!node || typeof node !== 'object') return;
340
+ const n = node as Record<string, unknown>;
341
+ const op = n.operation;
342
+ if (op && typeof op === 'object') {
343
+ const o = op as Record<string, unknown>;
344
+ if (o.operation === 'thread.start') {
345
+ const instance = (o.instance as number) ?? 0;
346
+ if (!threads.has(instance)) {
347
+ threads.set(instance, { instance, stackBytes: (o.stackBytes as number) ?? 2048, priority: (o.priority as number) ?? 5 });
348
+ }
349
+ }
350
+ }
351
+ for (const v of Object.values(n)) {
352
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
353
+ else if (v && typeof v === 'object') visit(v);
354
+ }
355
+ };
356
+ visit(program);
357
+ visitResolvedHalOps(program, visit);
358
+ return threads;
359
+ }
360
+ import { lowerHalOp } from './lowering/index.js';
361
+ import { adcInitLines, adcChannelForPin } from './lowering/adc.js';
362
+ import { pwmInitLines } from './lowering/pwm.js';
363
+ import { dacInitLines } from './lowering/dac.js';
364
+ import { fsInitLines } from './lowering/fs.js';
365
+ import { hwtimerInitLines } from './lowering/hwtimer.js';
366
+ import { i2cInitLines } from './lowering/i2c.js';
367
+ import { sensorStateLines } from './lowering/sensor.js';
368
+ import { spiTargetStateLines } from './lowering/spi.js';
369
+ import { threadStateLines } from './lowering/thread.js';
370
+ import { uartRingStateLines } from './lowering/uart.js';
371
+ import { spiInitLines } from './lowering/spi.js';
372
+ import { uartInitLines } from './lowering/uart.js';
373
+ import { usbInitLines, usbdDeviceLines } from './lowering/usb.js';
374
+
375
+ /**
376
+ * STM32F4 boot-time DBGMCU setup: set DBG_SLEEP|DBG_STOP|DBG_STANDBY
377
+ * (DBGMCU_CR @ 0xE0042004, bits 0–2) so SWD stays attachable while the app
378
+ * sleeps. RCC_APB1ENR (0x40023840) bit 18 clocks the DBGMCU first — F4 gates
379
+ * register access behind it. Raw-register form (not the STM32 LL headers) so
380
+ * the shim stays include-light; AUTOSAR-clean via reinterpret_cast.
381
+ */
382
+ function stm32f4DbgmcuLines(): string[] {
383
+ return [
384
+ '// CUTTLEFISH_STM32_DBGMCU_BEGIN',
385
+ '#include <zephyr/init.h>',
386
+ 'static int __tc_stm32_dbgmcu_keep_swd_alive(void) {',
387
+ ' volatile uint32_t* const rcc_apb1enr = reinterpret_cast<volatile uint32_t*>(0x40023840);',
388
+ ' *rcc_apb1enr = *rcc_apb1enr | (1UL << 18);',
389
+ ' volatile uint32_t* const dbgmcu_cr = reinterpret_cast<volatile uint32_t*>(0xE0042004);',
390
+ ' *dbgmcu_cr = *dbgmcu_cr | 0x7u;',
391
+ ' return 0;',
392
+ '}',
393
+ 'SYS_INIT(__tc_stm32_dbgmcu_keep_swd_alive, PRE_KERNEL_1, 0);',
394
+ '// CUTTLEFISH_STM32_DBGMCU_END',
395
+ ];
396
+ }
397
+ import { interruptInitLines, collectInterruptPins } from './lowering/interrupts.js';
398
+ import { wdtInitLines } from './lowering/wdt.js';
399
+ import { bleInitLines } from './lowering/ble.js';
400
+ import { wifiInitLines } from './lowering/wifi.js';
401
+ import { httpInitLines } from './lowering/http.js';
402
+ import { mqttInitLines } from './lowering/mqtt.js';
403
+ import { preferencesInitLines } from './lowering/preferences.js';
404
+ import { randomInitLines } from './lowering/random.js';
405
+ import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
406
+ import { resolveZephyrDisplayOp, newDisplayState, type DisplayState } from './display/index.js';
407
+ import { buildDisplayRuntime } from './display/gfx.js';
408
+ import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES } from './display/profiles.js';
409
+ import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
410
+ import { zephyrTouchAdapter } from './display/touch-adapter.js';
411
+
412
+ /** debugMode() memo — the boardgen facts lookup is not free and the method is
413
+ * called several times per build (transpile gate + artifact writers). */
414
+ const debugModeMemo = new Map<string, 'gdb' | 'none'>();
415
+
416
+ export class ZephyrStrategy implements PlatformStrategy {
417
+ readonly id = 'zephyr';
418
+
419
+ // ── Profile resolution ──────────────────────────────────────────────────
420
+
421
+ /**
422
+ * Resolve + cache the active chip from the platform context. Called lazily
423
+ * by the methods that need the descriptor (shimLines, resolveHALOperation
424
+ * via lowerHalOp).
425
+ *
426
+ * The chip descriptor derives from the board/MCU manifest's zephyr
427
+ * fields (via boardConstants). There is no registry fallback — a board
428
+ * that never generated a manifest stays NO_BOARD_CHIP.
429
+ */
430
+ /**
431
+ * Eagerly resolve + cache the chip for `program`. The emitter calls this
432
+ * before rendering any bodies — hal-op lowering reads the module-global
433
+ * chip cache, and the lazy resolve (inside shimLines) runs later than the
434
+ * first lowered statement.
435
+ */
436
+ prepareChip(program: ProgramIR | undefined, ctx?: PlatformContext): void {
437
+ this.resolveChip(ctx, program);
438
+ }
439
+
440
+ /**
441
+ * Contract-board generation: a custom PCB spec (typecad contract) names
442
+ * its wired pads and bus families; the SoC's bus controller labels come
443
+ * from the installed Zephyr tree's soc dtsi. Same builder as every board.
444
+ */
445
+ generateContractBoardModule(opts: {
446
+ soc: string;
447
+ zephyrBase: string;
448
+ pinNames: readonly string[];
449
+ padAliases?: readonly { exportName: string; padName: string }[];
450
+ peripherals: { i2c: boolean; spi: boolean; uart: boolean };
451
+ }): { boardTs: string; boardJson: string } {
452
+ return generateBoardModuleFromContract(opts);
453
+ }
454
+
455
+ private resolveChip(ctx?: PlatformContext, program?: ProgramIR) {
456
+ // The chip view reconstructs from the project's generated board
457
+ // manifest (boardgen's zephyr.* constants) — the one path, for every
458
+ // board. No curated registry, no target-based fallback: a program with
459
+ // no board data resolves NO_BOARD_CHIP and the lowering reports
460
+ // unsupported per subsystem.
461
+ const fromBoard = resolveChipFromBoard(program?.boardConstants);
462
+ setActiveChip(fromBoard ?? NO_BOARD_CHIP);
463
+ return fromBoard ?? NO_BOARD_CHIP;
464
+ }
465
+
466
+ forcedIncludes(_program?: ProgramIR, ctx?: PlatformContext): string[] {
467
+ // <zephyr/kernel.h> for k_msleep / k_uptime_get_32 / k_busy_wait / printk.
468
+ // <zephyr/drivers/gpio.h> for the gpio_pin_*_dt / gpio_dt_spec API.
469
+ // <cstdint> because DIRECT_CPP_TYPE_MAP passes int32_t/uint8_t through
470
+ // verbatim and Zephyr's minimal C++ lib provides it.
471
+ //
472
+ // Driver headers are usage-gated on ctx.analysis.usesX (same flags
473
+ // shimLines uses to emit the per-peripheral bus state), so an unused
474
+ // peripheral doesn't pull in its header. When analysis is absent (e.g. a
475
+ // capability query before a real build), the uses() helper defaults to
476
+ // true so nothing is stripped — mirrors framework-esp32's forcedIncludes.
477
+ const a = (ctx as any)?.analysis;
478
+ const uses = (f: string): boolean => (a ? !!a[f] : true);
479
+ const chip = this.resolveChip(ctx, _program);
480
+ // <zephyr/drivers/gpio.h> and <cstdint> stay unconditional: gpio.h is
481
+ // cross-cutting (gpio/power/interrupt/spi/pulse lowerings + the DT-spec
482
+ // machinery all reference its API, and no single usesX flag owns it), and
483
+ // the fixed-width types come via <zephyr/kernel.h> regardless — DIRECT_CPP_TYPE_MAP
484
+ // passes int32_t/uint8_t through verbatim.
485
+ const inc: string[] = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdint>'];
486
+ // <cstdio> backs the printf family only: __tc_print/__tc_println (emitted
487
+ // solely when the test-runner preprocessor injected them — tracked via
488
+ // usedPolyfillHelpers), raw printf/snprintf in user code (usesCstdio), and
489
+ // the fs/preferences/uart shims (their lowerings snprintf into buffers).
490
+ // A program touching none of those needs no <cstdio>.
491
+ const helpers = (a as { usedPolyfillHelpers?: Set<string> } | undefined)?.usedPolyfillHelpers;
492
+ const needsCstdio = uses('usesCstdio') || uses('usesFS') || uses('usesPreferences')
493
+ || uses('usesUart') || uses('usesUsb')
494
+ || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println');
495
+ if (needsCstdio) inc.push('<cstdio>');
496
+ if (uses('usesI2C')) inc.push('<zephyr/drivers/i2c.h>');
497
+ if (uses('usesSensor')) inc.push('<zephyr/drivers/sensor.h>');
498
+ if (uses('usesSPI')) inc.push('<zephyr/drivers/spi.h>');
499
+ if (uses('usesUart')) inc.push('<zephyr/drivers/uart.h>');
500
+ // USB CDC serial: the class instance is a UART device (uart.h); the
501
+ // device context macros + usbd_* API live in the next-stack header.
502
+ if (uses('usesUsb')) {
503
+ if (!inc.includes('<zephyr/drivers/uart.h>')) inc.push('<zephyr/drivers/uart.h>');
504
+ inc.push('<zephyr/usb/usbd.h>');
505
+ // touch-to-reset shim callback reboots via NVIC_SystemReset()
506
+ // (RAM-retaining Cortex-M reset — no CONFIG_REBOOT needed).
507
+ if (chip?.usb?.touchReset) inc.push('<cmsis_core.h>');
508
+ }
509
+ if (uses('usesADC')) inc.push('<zephyr/drivers/adc.h>');
510
+ if (uses('usesPWM')) inc.push('<zephyr/drivers/pwm.h>');
511
+ if (uses('usesDAC')) inc.push('<zephyr/drivers/dac.h>');
512
+ // Filesystem: littlefs on the storage partition. <cstring> backs the
513
+ // shim's strlen; the storage/flash_map + fs/littlefs headers carry the
514
+ // FIXED_PARTITION_ID macro + FS_LITTLEFS_DECLARE_DEFAULT_CONFIG the shim uses.
515
+ if (uses('usesFS')) inc.push('<zephyr/fs/fs.h>', '<zephyr/fs/littlefs.h>', '<zephyr/storage/flash_map.h>', '<cstring>');
516
+ // Hardware timers via the counter driver.
517
+ if (uses('usesHwtimer')) inc.push('<zephyr/drivers/counter.h>');
518
+ if (uses('usesWDT') || uses('usesWdt')) inc.push('<zephyr/drivers/watchdog.h>');
519
+ // BLE: the bt_* GATT API + the flat-string headers the shim uses. <string>
520
+ // is needed because a Utf8 (BleValueType.Utf8) read handler lowers to a
521
+ // std::string-returning function (the string literal return type), and the
522
+ // program-analysis usesStdString detector doesn't see types generated by
523
+ // the BLE lowering layer — so without forcing <string> here, any BLE server
524
+ // with a Utf8 characteristic fails to compile ('std::string does not name a
525
+ // type'). <cstdlib>/<cstring> (not <stdlib.h>/<string.h>) back the shim's
526
+ // strtol/strcmp/strncpy — the same AUTOSAR-compliant spelling the HTTP,
527
+ // MQTT, and Preferences paths below already use.
528
+ if (uses('usesBle')) inc.push('<cstdlib>', '<cstring>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
529
+ // Display: the analyzer's usesDisplay flag (set by display.* hal-ops) drives
530
+ // this include. When ctx.analysis is absent (capability query), uses()
531
+ // defaults to true so a real build never strips it.
532
+ if (uses('usesDisplay')) inc.push('<zephyr/drivers/display.h>');
533
+ // Random: <zephyr/random/random.h> for sys_rand_get (the entropy tap that
534
+ // seeds the __tc_rand_* xorshift32 PRNG). The shim block re-includes it, but
535
+ // force it here too so a split-TU emit still has the symbol available.
536
+ if (uses('usesRandom')) inc.push('<zephyr/random/random.h>');
537
+ if (uses('usesWifi')) inc.push(
538
+ '<zephyr/net/net_mgmt.h>', '<zephyr/net/wifi_mgmt.h>',
539
+ '<zephyr/net/net_if.h>', '<zephyr/net/net_ip.h>',
540
+ '<zephyr/net/conn_mgr_connectivity.h>', '<zephyr/net/conn_mgr_monitor.h>',
541
+ );
542
+ // HTTP/S client: Zephyr's http_client_req runs over a pre-connected socket,
543
+ // so the shim pulls in the BSD socket + POSIX DNS surfaces alongside the
544
+ // http client/parser headers. TLS sec tags need tls_credentials; <cstring>
545
+ // /<cstdlib>/<new> back the shim's memcpy/strlen/new-nothrow usage (the
546
+ // core shim only includes <cstdio>/<cstdint> — std::nothrow is not
547
+ // guaranteed to arrive transitively).
548
+ if (uses('usesHttp')) inc.push(
549
+ '<zephyr/net/socket.h>', '<zephyr/net/http/client.h>',
550
+ '<zephyr/net/http/parser.h>', '<zephyr/net/tls_credentials.h>',
551
+ '<zephyr/posix/sys/socket.h>', '<cstring>', '<cstdlib>', '<new>',
552
+ );
553
+ // MQTT client: <zephyr/net/mqtt.h> for mqtt_connect/publish/subscribe, plus
554
+ // <zephyr/net/socket.h> for the zsock_* poll/getaddrinfo API the shim's poll
555
+ // thread uses, and <zephyr/net/tls_credentials.h> for the mqtts:// path
556
+ // (mqtt_sec_config). <cstring> backs the shim's memcpy/strlen.
557
+ if (uses('usesMqtt')) inc.push(
558
+ '<zephyr/net/mqtt.h>', '<zephyr/net/socket.h>',
559
+ '<zephyr/net/tls_credentials.h>', '<cstring>',
560
+ );
561
+ // Preferences: Zephyr settings subsystem (ZMS backend) — settings_load/
562
+ // settings_save_one/settings_delete + the SETTINGS_STATIC_HANDLER_DEFINE
563
+ // macro. <cstring> backs the shim's memcpy/memmove/strncpy/strncmp (the
564
+ // core shim only includes <cstdio>/<cstdint>). <errno.h> for ENOENT in h_get.
565
+ if (uses('usesPreferences')) inc.push(
566
+ '<zephyr/settings/settings.h>', '<cstring>', '<errno.h>',
567
+ );
568
+ // std::string — Zephyr has no umbrella header that transitively pulls in
569
+ // <string> (unlike framework-arduino's <Arduino.h>), so a program that
570
+ // lowers a std::string parameter/variable must request it explicitly. Uses
571
+ // <string>, not <string.h>: the latter is the C flat-string header.
572
+ if (uses('usesStdString')) inc.push('<string>');
573
+ return inc;
574
+ }
575
+
576
+ symbolAliases(): Record<string, string> {
577
+ return {};
578
+ }
579
+
580
+
581
+ /**
582
+ * Detect async-runtime usage: a program needs the Promise/microtask runtime
583
+ * if it declares an async function OR references an async-runtime symbol
584
+ * (`__cuttlefish_async_`) — e.g. Async.sleep()/.then() called from a non-async
585
+ * function. Mirrors Arduino's programUsesAsyncRuntime walk (that helper is
586
+ * private to framework-arduino and not exported from cuttlefish, so we walk
587
+ * here). The token appears in `raw` expr nodes and in `hal-expr`/`hal-op`
588
+ * nodes whose resolved `raw` code references it.
589
+ */
590
+ private programUsesAsyncRuntime(program?: ProgramIR): boolean {
591
+ if (!program) return false;
592
+ const TOKEN = '__cuttlefish_async_';
593
+ let found = false;
594
+ const visit = (node: any): void => {
595
+ if (found || !node || typeof node !== 'object') return;
596
+ // raw expression node
597
+ if (node.kind === 'raw' && typeof node.value === 'string' && node.value.includes(TOKEN)) {
598
+ found = true; return;
599
+ }
600
+ // hal-expr / hal-op node whose operation is a raw op carrying code
601
+ if (node.operation && typeof node.operation === 'object'
602
+ && node.operation.operation === 'raw'
603
+ && typeof node.operation.code === 'string'
604
+ && node.operation.code.includes(TOKEN)) {
605
+ found = true; return;
606
+ }
607
+ for (const v of Object.values(node)) {
608
+ if (typeof v === 'string' && v.includes(TOKEN)) { found = true; return; }
609
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
610
+ else if (v && typeof v === 'object') visit(v);
611
+ }
612
+ };
613
+ visit(program);
614
+ return found;
615
+ }
616
+
617
+ /** Pins referenced by gpio.* hal-ops in the program IR. lowerGpio routes a
618
+ * pin to its devicetree spec by pin NUMBER, so the structured hal-op pins
619
+ * are the authoritative signal for which __tc_dt_* specs are needed —
620
+ * regardless of when the final call text is rendered.
621
+ *
622
+ * A gpio op interpolated into another HAL call's template literal
623
+ * (`USB0.writeLine(\`led: ${led.get()}\`)`) never becomes an IR node —
624
+ * resolveHALExprToText lowers it to C++ text while building the IR and bakes
625
+ * it into an __EMIT__ snprintf prelude, so the walk cannot see its pin. The
626
+ * spec NAME it references is still in that text; collectRawMatches finds it
627
+ * there, or the spec goes undeclared and the build fails on `__tc_dt_led0
628
+ * was not declared in this scope`. */
629
+ private collectGpioPinUsage(program?: ProgramIR): Set<number> {
630
+ const pins = new Set<number>();
631
+ if (!program) return pins;
632
+ const visit = (node: any): void => {
633
+ if (!node || typeof node !== 'object') return;
634
+ if (node.operation && typeof node.operation === 'object'
635
+ && typeof node.operation.operation === 'string'
636
+ && node.operation.operation.startsWith('gpio.')
637
+ && typeof node.operation.pin === 'number') {
638
+ pins.add(node.operation.pin);
639
+ }
640
+ for (const v of Object.values(node)) {
641
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
642
+ else if (v && typeof v === 'object') visit(v);
643
+ }
644
+ };
645
+ visit(program);
646
+ visitResolvedHalOps(program, visit);
647
+ return pins;
648
+ }
649
+
650
+ /** Run `re` (global) against every raw string in the IR — raw expression
651
+ * values plus raw hal-op codes — returning capture group 1 of each match
652
+ * (the full match when the regex has no group). This is how references the
653
+ * text scanners must see but that never appear as IR call nodes are
654
+ * discovered: a rawCpp() escape hatch naming `__tc_dt_sw0` directly, and —
655
+ * the reason every string LEAF is scanned, not just raw nodes — the
656
+ * pre-lowered C++ text of a HAL call inlined into another HAL call's
657
+ * template literal, which the IR builder bakes into plain `string` payloads
658
+ * (__EMIT__ snprintf preludes, op data fields) rather than raw nodes. */
659
+ private collectRawMatches(program: ProgramIR | undefined, re: RegExp): Set<string> {
660
+ const found = new Set<string>();
661
+ if (!program) return found;
662
+ const scan = (text: string): void => {
663
+ for (const m of text.matchAll(re)) found.add(m[1] ?? m[0]);
664
+ };
665
+ const visit = (node: any): void => {
666
+ if (!node || typeof node !== 'object') return;
667
+ for (const v of Object.values(node)) {
668
+ if (typeof v === 'string') scan(v);
669
+ else if (Array.isArray(v)) { for (const item of v) visit(item); }
670
+ else if (v && typeof v === 'object') visit(v);
671
+ }
672
+ };
673
+ visit(program);
674
+ return found;
675
+ }
676
+
677
+ /** Whether the wiring-compat GPIO read surface (__tc_gpio_read definition,
678
+ * __tc_gpio_dev dispatcher, and the wiring_compat polyfill's digitalRead /
679
+ * HIGH / LOW macros) must be emitted. Consumers: user digitalRead() calls
680
+ * (usesDigitalRead), the @typecad/safety voter (calls __tc_gpio_read
681
+ * directly via lowered raw text), and the UI runtime header's
682
+ * unconditional digitalRead() poll (entryHasUI — build-global, so every TU
683
+ * in a UI build carries the macros). With no analysis present (capability
684
+ * query), default to emitting — same convention as the uses() helper. */
685
+ private needsGpioReadShim(program?: ProgramIR, ctx?: PlatformContext): boolean {
686
+ if (program && programUsesSafety(program)) return true;
687
+ if (entryHasUI()) return true;
688
+ const a = (ctx as any)?.analysis;
689
+ return a ? !!a.usesDigitalRead : true;
690
+ }
691
+
692
+ shimLines(program?: ProgramIR, ctx?: PlatformContext): string[] {
693
+ const chip = this.resolveChip(ctx, program);
694
+ const a = (ctx as any)?.analysis;
695
+ const uses = (f: string): boolean => (a ? !!a[f] : true);
696
+ const helpers = (a as { usedPolyfillHelpers?: Set<string> } | undefined)?.usedPolyfillHelpers;
697
+
698
+ // --- Core shim, gated item by item on actual use ------------------------
699
+ // A minimal program (blink) uses none of these, and its output carries no
700
+ // shim block at all. Everything up to the #endif composes into one guard
701
+ // body; the guard itself is only stamped when the body is non-empty.
702
+ const guardBody: string[] = [];
703
+ // CUTTLEFISH_UNDEFINED: needed when the file references null/undefined
704
+ // literals (usesNullish), emits nullish helper CALLS (usesNullishHelper),
705
+ // or has async functions (the async state machine uses the macro for
706
+ // default waitFor* timeouts — not visible to the nullish scanners).
707
+ if (uses('usesNullish') || uses('usesNullishHelper') || uses('hasAsync')) {
708
+ guardBody.push(
709
+ '#ifndef CUTTLEFISH_UNDEFINED',
710
+ '#define CUTTLEFISH_UNDEFINED 0',
711
+ '#endif',
712
+ );
713
+ }
714
+ // Nullish helpers: only when the file actually emits cuttlefish_nullish /
715
+ // cuttlefish_exists CALLS (?? / ?. lowering). A file that only references
716
+ // null/undefined literals needs just the macro above — the same
717
+ // distinction the setup emitter's strip filter documents.
718
+ if (uses('usesNullishHelper')) {
719
+ guardBody.push(
720
+ 'template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }',
721
+ 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }',
722
+ 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }',
723
+ 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }',
724
+ 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }',
725
+ 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }',
726
+ 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }',
727
+ 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
728
+ );
729
+ }
730
+ // The runtime clock contract: __tc_now_ms() is the monotonic ms clock
731
+ // the shared runtimes (UI per-frame tick, async timers, scheduler)
732
+ // and currentTimeMillis() lower onto. Emitted whenever anything could
733
+ // read the clock: explicit Time calls, async, the interval/timeout
734
+ // scheduler, or a mounted UI. uint32_t wraps every ~49.7 days — every
735
+ // consumer compares by subtraction, so the wrap is harmless.
736
+ if (uses('usesWallClock') || uses('hasAsync') || (!a || a.timerCallCount > 0)
737
+ || this.programUsesAsyncRuntime(program) || entryHasUI()) {
738
+ guardBody.push(
739
+ 'inline uint32_t __tc_now_ms(void) { return k_uptime_get_32(); }',
740
+ );
741
+ }
742
+ // Num fluent math (Num.abs/min/max) + the free math trio.
743
+ // On Zephyr newlib none of these
744
+ // names are macros, so the underscore-escape rewrite the
745
+ // strategy needs is not. abs(long)/min/max deliberately overload (not
746
+ // shadow) newlib's abs(int) — exact-match int calls still resolve to the
747
+ // libc overload.
748
+ if (uses('usesNum')) {
749
+ guardBody.push(
750
+ 'struct __tc_Num {',
751
+ ' static long abs(long x) { return x < 0 ? -x : x; }',
752
+ ' static long min(long a, long b) { return a < b ? a : b; }',
753
+ ' static long max(long a, long b) { return a > b ? a : b; }',
754
+ // The core emitter escapes abs/min/max member calls to a trailing
755
+ // underscore (C-library collision) — provide the escaped spellings so
756
+ // Num.abs_()/Num.min_()/Num.max_() resolve too.
757
+ ' static long abs_(long x) { return x < 0 ? -x : x; }',
758
+ ' static long min_(long a, long b) { return a < b ? a : b; }',
759
+ ' static long max_(long a, long b) { return a > b ? a : b; }',
760
+ '} Num;',
761
+ 'inline long abs(long v) { return v < 0 ? -v : v; }',
762
+ 'inline long min(long a, long b) { return a < b ? a : b; }',
763
+ 'inline long max(long a, long b) { return a > b ? a : b; }',
764
+ // Escaped free-function spellings — the escape applies to bare call
765
+ // sites on some paths (abs_(-42)); cover both.
766
+ 'inline long abs_(long v) { return v < 0 ? -v : v; }',
767
+ 'inline long min_(long a, long b) { return a < b ? a : b; }',
768
+ 'inline long max_(long a, long b) { return a > b ? a : b; }',
769
+ );
770
+ }
771
+ // Random PRNG shim: the Random namespace lowers to the __tc_rand_* PRNG.
772
+ // Emitted when the program actually uses the random.* ops, so a minimal
773
+ // program's shim stays empty. The dispatcher + PRNG helpers must precede
774
+ // the shims that call them (the guard block emits in push order, and
775
+ // C++ needs the definitions before use).
776
+ if (this.needsGpioReadShim(program, ctx)) {
777
+ guardBody.push(...emitGpioDevDispatcher(chip));
778
+ }
779
+ if (uses('usesRandom')) {
780
+ guardBody.push(...randomInitLines());
781
+ }
782
+ // PROGMEM note: only some UI runtime headers reference it.
783
+ if (entryHasUI()) {
784
+ guardBody.push(
785
+ '#ifndef PROGMEM', '#define PROGMEM', '#endif',
786
+ );
787
+ }
788
+ // Test-runner console helpers: the test-runner's Zephyr shim calls these
789
+ // for protocol output. Overloaded for string (const char*) and numeric
790
+ // (double) so the same call site works for markers and test values.
791
+ // The fs lowering bakes __tc_println into its error paths too, so the
792
+ // overloads are emitted unconditionally — ctx.analysis can be absent or
793
+ // stale on UI programs, and missing definitions would be a link error.
794
+ //
795
+ // The numeric form formats via INTEGER conversions only: libc float
796
+ // printf is not dependable across SDKs — the 0.17.5 toolchain swapped
797
+ // newlib for picolibc, whose default build silently prints NOTHING for
798
+ // %g (the same trap as newlib-nano's -u _printf_float), which emptied
799
+ // every [TC:EXPECT:...:value:] line. Integer %lld works in every libc
800
+ // configuration, and the host parser accepts plain fixed-point.
801
+ {
802
+ guardBody.push(
803
+ 'inline void __tc_print(const char* s) { printf("%s", s); }',
804
+ [
805
+ 'static inline char* __tc_fmt_num_buf(double v, char* out, size_t cap) {',
806
+ ' if (v != v) { snprintf(out, cap, "nan"); return out; }',
807
+ ' double a = v < 0 ? -v : v;',
808
+ ' long long ip = (long long)a;',
809
+ ' long long fr = (long long)((a - (double)ip) * 1000000.0 + 0.5);',
810
+ ' if (fr >= 1000000LL) { ip += 1LL; fr = 0LL; }',
811
+ ' int used = 0;',
812
+ ' if (v < 0 && (ip != 0LL || fr != 0LL)) { out[used++] = \'-\'; out[used] = \'\\0\'; }',
813
+ ' if (fr == 0LL) { snprintf(out + used, cap - (size_t)used, "%lld", ip); return out; }',
814
+ ' char fbuf[8];',
815
+ ' int len = snprintf(fbuf, sizeof(fbuf), "%06lld", fr);',
816
+ ' while (len > 0 && fbuf[len - 1] == \'0\') { fbuf[--len] = \'\\0\'; }',
817
+ ' snprintf(out + used, cap - (size_t)used, "%lld.%s", ip, fbuf);',
818
+ ' return out;',
819
+ '}',
820
+ 'static void __tc_fmt_num(double v) { char __b[32]; printf("%s", __tc_fmt_num_buf(v, __b, sizeof(__b))); }',
821
+ ].join('\n'),
822
+ 'inline void __tc_print(double v) { __tc_fmt_num(v); }',
823
+ 'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
824
+ 'inline void __tc_println(double v) { __tc_fmt_num(v); printf("\\n"); }',
825
+ );
826
+ }
827
+ // Serial-port write helper: writes a scalar to a UART/CDC device a byte at
828
+ // a time. Overloaded on const char* (strings, snprintf buffers) and double
829
+ // (numbers/booleans) so a single __tc_dev_put(dev, value) call site formats
830
+ // any writable scalar — the same overload contract __tc_print uses. Emitted
831
+ // when the program writes a UART or USB port (both include uart.h).
832
+ if (uses('usesUart') || uses('usesUsb')) {
833
+ guardBody.push(
834
+ 'static inline void __tc_dev_put(const struct device* dev, const char* s) {',
835
+ ' for (; *s != \'\\0\'; ++s) { uart_poll_out(dev, *s); }',
836
+ '}',
837
+ 'static inline void __tc_dev_put(const struct device* dev, double v) {',
838
+ ' char __b[32];',
839
+ ' __tc_dev_put(dev, __tc_fmt_num_buf(v, __b, sizeof(__b)));',
840
+ '}',
841
+ );
842
+ }
843
+
844
+ // Per-peripheral bus state — gated on the same ctx.analysis.usesX flags as
845
+ // forcedIncludes, so an unused peripheral emits no state (and its header is
846
+ // not included). Mirrors framework-esp32's shimLines espInit block. Within
847
+ // a bus, only the instances the program drives get state — an unused
848
+ // declared controller's __tc_<bus>N_dev trips -Wunused-variable under
849
+ // Zephyr's -Werror (collectUsedBusIndices; probes with no program emit all).
850
+ const usedBuses = collectUsedBusIndices(program);
851
+ if (uses('usesI2C') && chip.i2c) {
852
+ for (let i = 0; i < chip.i2c.controllers.length; i++) {
853
+ if (usedBuses && !usedBuses.i2c.has(i)) continue;
854
+ guardBody.push(...i2cInitLines(chip, i));
855
+ }
856
+ }
857
+ if (uses('usesSPI') && chip.spi) {
858
+ for (let i = 0; i < chip.spi.controllers.length; i++) {
859
+ if (usedBuses && !usedBuses.spi.has(i)) continue;
860
+ guardBody.push(...spiInitLines(chip, i));
861
+ }
862
+ }
863
+ if (uses('usesUart') && chip.uart) {
864
+ for (let i = 0; i < chip.uart.controllers.length; i++) {
865
+ if (usedBuses && !usedBuses.uart.has(i)) continue;
866
+ guardBody.push(...uartInitLines(chip, i));
867
+ }
868
+ // Thin UART RX rings — one interrupt-drained ring per port the program
869
+ // reads (uart.rx_* ops). Emitted after the device handles it references.
870
+ const rings = collectUartRings(program);
871
+ if (rings) {
872
+ for (const r of rings.values()) {
873
+ guardBody.push(...uartRingStateLines(r.index, r.ring));
874
+ }
875
+ }
876
+ }
877
+ // DT-bound sensors — one state block per constructed part (device handle
878
+ // + sensor_value scratch). The lowering's __tc_sensor_* references and the
879
+ // overlay's DT child nodes derive from the same facts (lowering/sensor.ts).
880
+ if (uses('usesSensor')) {
881
+ const sensors = collectSensors(program);
882
+ if (sensors) {
883
+ for (const s of sensors.values()) {
884
+ guardBody.push(...sensorStateLines(s.part, s.bus, s.port, s.busKind, s.spiHz, s.spiMode, s.alertPin));
885
+ }
886
+ }
887
+ }
888
+ // Thin SPI targets — one spi_dt_spec per constructed target, against the
889
+ // DT child node the overlay emits. Same shared-facts discipline (the
890
+ // tc-spit-cfg comment is the overlay scanner's channel).
891
+ if (uses('usesSPI')) {
892
+ const targets = collectSpiTargets(program);
893
+ if (targets) {
894
+ for (const t of targets.values()) {
895
+ guardBody.push(...spiTargetStateLines(t.bus, t.cs, t.hz, t.mode));
896
+ }
897
+ }
898
+ }
899
+ // Thin Threads — one stack + k_thread + trampoline per started slot.
900
+ // Keyed on thread.start ops only: join() without a prior start() on the
901
+ // same index is a user error that surfaces as the undefined slot symbol.
902
+ {
903
+ const threads = collectThreads(program);
904
+ if (threads) {
905
+ for (const t of threads.values()) {
906
+ guardBody.push(...threadStateLines(t.instance, t.stackBytes));
907
+ }
908
+ }
909
+ }
910
+ if (uses('usesUsb') && chip.usb) {
911
+ guardBody.push(...usbdDeviceLines(chip));
912
+ for (let i = 0; i < chip.usb.cdcInstances; i++) guardBody.push(...usbInitLines(chip, i));
913
+ }
914
+ // PWM init also fires (with alias vars for the override pins) when a
915
+ // program drives ONLY inline-override channels on a chip with no pwm
916
+ // facts — the lowered calls reference those aliases.
917
+ {
918
+ const pwmUserSpecs = collectPwmOverrideSpecs(program);
919
+ if ((uses('usesPWM') && chip.pwm) || pwmUserSpecs.length > 0) {
920
+ guardBody.push(...pwmInitLines(chip, collectUsedPins(program, 'pwm', chip), pwmUserSpecs));
921
+ }
922
+ }
923
+ if (uses('usesDAC') && chip.dac) guardBody.push(...dacInitLines(chip));
924
+ if (uses('usesHwtimer') && chip.hwtimer) guardBody.push(...hwtimerInitLines(chip));
925
+ if (uses('usesInterrupts')) guardBody.push(...interruptInitLines(chip, program ? collectInterruptPins(program) : undefined));
926
+ if ((uses('usesWDT') || uses('usesWdt')) && chip.wdt) guardBody.push(...wdtInitLines(chip));
927
+ if (uses('usesBle')) guardBody.push(...bleInitLines());
928
+ // Display runtime (rect/text renderer): the DIRECT-call display path (user
929
+ // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
930
+ // when the program uses display.* but is NOT a UI program — the UI display
931
+ // adapter (emitted by cuttlefish's emitUIRuntime, solely under entryHasUI())
932
+ // defines the same display_init symbol, so emitting both would collide.
933
+ // `providesDisplayAdapter()` is a static capability (always true here) and
934
+ // does NOT track whether the adapter is actually emitted for THIS build, so
935
+ // the per-program UI signal (entryHasUI) is the correct gate. Without this,
936
+ // a direct display.* program has no definition for display_init/
937
+ // display_fill_rect/draw_rect/draw_text/flush (the gfx runtime was
938
+ // previously dead code).
939
+ if (uses('usesDisplay') && !entryHasUI()) {
940
+ const rt = buildDisplayRuntime(this._displayState.profile);
941
+ guardBody.push(...rt.stateLines);
942
+ guardBody.push(rt.fontTable);
943
+ guardBody.push(rt.helpers);
944
+ }
945
+ if (uses('usesWifi')) guardBody.push(...wifiInitLines());
946
+ if (uses('usesHttp')) guardBody.push(...httpInitLines());
947
+ if (uses('usesMqtt')) guardBody.push(...mqttInitLines());
948
+ if (uses('usesPreferences')) guardBody.push(...preferencesInitLines());
949
+ if (uses('usesFS')) guardBody.push(...fsInitLines());
950
+ // The ADC init block also fires (with the override devices' handles)
951
+ // when a program uses ONLY inline-override reads on a chip with no adc
952
+ // facts — otherwise those reads reference undeclared device handles.
953
+ {
954
+ const adcOverrideDevices = collectAdcOverrideDevices(program);
955
+ if ((uses('usesADC') && chip.adc) || adcOverrideDevices.size > 0) {
956
+ guardBody.push(...adcInitLines(chip, collectUsedPins(program, 'adc'), adcOverrideDevices));
957
+ }
958
+ }
959
+ // STM32F4: keep the core debug port alive across WFI sleep. The DBGMCU
960
+ // gates PPB access while the core sleeps unless DBGMCU_CR DBG_SLEEP/
961
+ // DBG_STOP/DBG_STANDBY are set — without them openocd cannot examine or
962
+ // halt the running target ("Failed to read memory at 0xe000ed04", "AP
963
+ // write error, reset will not halt"), and with no RST pad on boards like
964
+ // the Black Pill the only recovery is the BOOT0 bootloader. Zephyr's
965
+ // CONFIG_STM32_ENABLE_DEBUG_SLEEP_STOP sets only DBG_STOP on F4 (the
966
+ // soc_config.c F1/L1 branch is the one that sets DBG_SLEEP), so the bits
967
+ // are set here at boot, unconditionally for dev boards.
968
+ // (soc is empty on board-resolved chips — the SoC name rides in the
969
+ // qualified id's variant segment, e.g. 'blackpill_f411ce/stm32f411xe'.)
970
+ if (chip.soc.startsWith('stm32f4') || /stm32f4\d*/.test(chip.id)) {
971
+ guardBody.push(...stm32f4DbgmcuLines());
972
+ }
973
+
974
+ const lines: string[] = [];
975
+ if (guardBody.length > 0) {
976
+ lines.push(
977
+ '// cuttlefish runtime shim. Wrapped in a single include guard so the',
978
+ '// block is safe to emit into multiple headers and .cpp files within',
979
+ '// one translation unit (a .cpp may #include several headers that each',
980
+ '// carry the shim). The guard ensures the definitions are seen exactly',
981
+ '// once per TU.',
982
+ '#ifndef CUTTLEFISH_SHIM_DEFINED',
983
+ '#define CUTTLEFISH_SHIM_DEFINED',
984
+ ...guardBody,
985
+ '#endif // CUTTLEFISH_SHIM_DEFINED',
986
+ );
987
+ }
988
+
989
+ // Devicetree specs — one per board-defined GPIO pin, but ONLY for pins the
990
+ // program actually addresses (lowerGpio routes by pin number; the
991
+ // structured gpio.* hal-op pins plus the spec names appearing in any IR
992
+ // string — including the pre-lowered text of a HAL call inlined into
993
+ // another HAL call's template literal — are visible here) and aliases
994
+ // named verbatim in raw code (rawCpp escape hatches). Emitted OUTSIDE the
995
+ // single CUTTLEFISH_SHIM_DEFINED guard with a per-symbol guard: per-file
996
+ // pin sets differ, and in a multi-header TU the first header's TU-wide
997
+ // guard would otherwise hide the second header's specs. Without a program
998
+ // (capability query), emit them all.
999
+ const usedPins = this.collectGpioPinUsage(program);
1000
+ const dtTextRefs = this.collectRawMatches(program, /__tc_dt_([A-Za-z0-9_]+)/g);
1001
+ for (const spec of chip.gpio.dtSpecs) {
1002
+ if (program && !usedPins.has(spec.pin) && !dtTextRefs.has(spec.dtSpec)) continue;
1003
+ const guard = `__TC_DT_${spec.dtSpec.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_SPEC`;
1004
+ lines.push(
1005
+ `#ifndef ${guard}`,
1006
+ `#define ${guard}`,
1007
+ `static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`,
1008
+ `#endif // ${guard}`,
1009
+ );
1010
+ }
1011
+
1012
+
1013
+ // --- Zephyr entrypoint ---------------------------------------------------
1014
+ // No bridge here: the synthesizer emits main() itself (it keys off
1015
+ // entrypointFunctionName()="main" and requiresLoopFunction()=false), with
1016
+ // the top-level statements inlined. Per-frame driving (ui_tick, microtask
1017
+ // pumping, async task .run()) is appended to main() by the driver
1018
+ // machinery via asyncDriverFunctionName()="main" + hostEventLoop() — see
1019
+ // asyncLoopInjection for the scheduler loop it closes over.
1020
+
1021
+ // GPIO read shim: emitted only when something actually reads a pin at
1022
+ // runtime — user digitalRead() calls, the @typecad/safety voter (calls
1023
+ // __tc_gpio_read directly), or the UI runtime header's digitalRead() poll
1024
+ // (init-press-input.ts). A program that only writes/toggles GPIO needs
1025
+ // neither the dispatcher nor the reader.
1026
+ //
1027
+ // The signature is `int` to match wiring_compat's forward declaration —
1028
+ // a uint32_t definition alongside it would leave the declared int
1029
+ // overload undefined (int wins overload resolution for small integer
1030
+ // arguments).
1031
+ //
1032
+ // The pin is a RUNTIME value here (the UI pin-watch table and safety's
1033
+ // voter pass whatever pin they were handed), so the controller cannot be
1034
+ // baked in as a single DT_NODELABEL on a multi-controller SoC (ESP32-S3:
1035
+ // pins 0–31 → gpio0, 32–48 → gpio1). Emit a tiny __tc_gpio_dev(pin)
1036
+ // dispatcher that resolves the owning controller's device per pin;
1037
+ // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
1038
+ // still compile-time-resolved per branch, so it is always statically valid.
1039
+ // (The dispatcher itself now lives at the top of the guard block — it must
1040
+ // precede the wiring-ambient shims and the PinGroup factory there; the
1041
+ // __tc_gpio_read body below is what still needs emitting here.)
1042
+ if (this.needsGpioReadShim(program, ctx)) {
1043
+ lines.push(
1044
+ 'inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), __tc_gpio_pin(static_cast<uint32_t>(pin))); }',
1045
+ );
1046
+ }
1047
+ // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
1048
+ // lowering, so they stay gated on it.
1049
+ if (program && programUsesSafety(program)) {
1050
+ lines.push(
1051
+ 'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), __tc_gpio_pin(pin), value); }',
1052
+ '#ifndef __TC_DELAY_US_DEFINED',
1053
+ '#define __TC_DELAY_US_DEFINED',
1054
+ 'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }',
1055
+ '#endif',
1056
+ );
1057
+ }
1058
+
1059
+ return lines;
1060
+ }
1061
+
1062
+ profileDiagnostics(program?: ProgramIR, ctx?: PlatformContext): Diagnostic[] {
1063
+ if (!program) return [];
1064
+ const chip = this.resolveChip(ctx, program);
1065
+ const a = (ctx as any)?.analysis ?? {};
1066
+ const diags: Diagnostic[] = [];
1067
+
1068
+ // Collect the pins the program uses for output config, ADC reads, and
1069
+ // interrupt attaches — deep-walking the IR the same way framework-esp32
1070
+ // does (its profileDiagnostics walks program to find gpio.set_mode /
1071
+ // adc.read nodes).
1072
+ const outputPins = new Set<number>();
1073
+ const adcReadPins = new Set<number>();
1074
+ const adcOverriddenPins = new Set<number>();
1075
+ const pwmOverriddenPins = new Set<number>();
1076
+ const interruptPins = new Set<number>();
1077
+ const wdtOps = new Set<string>();
1078
+ const dacPins = new Set<number>();
1079
+ const pwmPins = new Set<number>();
1080
+ const busInstances = { i2c: new Set<number>(), spi: new Set<number>(), uart: new Set<number>() };
1081
+ const hwtimerInstances = new Set<number>();
1082
+ let usesWifiOps = false;
1083
+ let usesHttpOps = false;
1084
+ let usesMqttOps = false;
1085
+ const visit = (node: any): void => {
1086
+ if (node && typeof node === 'object') {
1087
+ if (node.operation && typeof node.operation === 'object') {
1088
+ const op = node.operation;
1089
+ if (op.operation === 'gpio.set_mode'
1090
+ && typeof op.mode === 'string'
1091
+ && op.mode.toLowerCase() === 'output'
1092
+ && typeof op.pin === 'number') {
1093
+ outputPins.add(op.pin);
1094
+ }
1095
+ if ((op.operation === 'adc.read' || op.operation === 'adc.read_voltage'
1096
+ || op.operation === 'adc.read_raw' || op.operation === 'adc.read_mv')
1097
+ && typeof op.pin === 'number') {
1098
+ adcReadPins.add(op.pin);
1099
+ // Inline routing overrides (construction opts): the user vouches
1100
+ // for the pin — the unavailable-pin diagnostic does not apply.
1101
+ if ((typeof op.channelOverride === 'number' && op.channelOverride >= 0)
1102
+ || (typeof op.deviceOverride === 'string' && op.deviceOverride !== '')) {
1103
+ adcOverriddenPins.add(op.pin);
1104
+ }
1105
+ }
1106
+ if (op.operation === 'interrupt.attach_flags' && typeof op.pin === 'number') {
1107
+ interruptPins.add(op.pin);
1108
+ }
1109
+ if (typeof op.operation === 'string' && op.operation.startsWith('wdt.')) {
1110
+ wdtOps.add(op.operation);
1111
+ }
1112
+ if ((op.operation === 'dac.write' || op.operation === 'dac.write_value')
1113
+ && typeof op.pin === 'number') {
1114
+ dacPins.add(op.pin);
1115
+ }
1116
+ if ((op.operation === 'pwm.set_pulse' || op.operation === 'pwm.set_duty'
1117
+ || op.operation === 'pwm.set_period')
1118
+ && typeof op.pin === 'number') {
1119
+ pwmPins.add(op.pin);
1120
+ if ((typeof op.controllerOverride === 'string' && op.controllerOverride !== '')
1121
+ || (typeof op.channelOverride === 'number' && op.channelOverride >= 0)) {
1122
+ pwmOverriddenPins.add(op.pin);
1123
+ }
1124
+ }
1125
+ // Bus instance usage: which I2C/SPI/UART controller indexes the
1126
+ // program drives (the lowering resolves index N against
1127
+ // chip.<bus>.controllers[N]; an undeclared index emits references
1128
+ // to a state block that is never declared → link error).
1129
+ if (typeof op.operation === 'string') {
1130
+ const busMatch = op.operation.match(/^(i2c|spi|uart)\./);
1131
+ if (busMatch) {
1132
+ const bus = busMatch[1] as 'i2c' | 'spi' | 'uart';
1133
+ const id = typeof op.bus === 'string' ? op.bus
1134
+ : typeof op.port === 'string' ? op.port : undefined;
1135
+ const m = id?.match(/(\d+)$/);
1136
+ if (m) busInstances[bus].add(parseInt(m[1], 10));
1137
+ }
1138
+ }
1139
+ if (typeof op.operation === 'string' && op.operation.startsWith('hwtimer.')) {
1140
+ const inst = typeof op.instance === 'number'
1141
+ ? op.instance
1142
+ : parseInt(String(op.instance), 10);
1143
+ if (!isNaN(inst)) hwtimerInstances.add(inst);
1144
+ }
1145
+ if (typeof op.operation === 'string' && op.operation.startsWith('wifi.')) {
1146
+ usesWifiOps = true;
1147
+ }
1148
+ if (typeof op.operation === 'string' && op.operation.startsWith('http.')) {
1149
+ usesHttpOps = true;
1150
+ }
1151
+ if (typeof op.operation === 'string' && op.operation.startsWith('mqtt.')) {
1152
+ usesMqttOps = true;
1153
+ }
1154
+ }
1155
+ for (const k of Object.keys(node)) {
1156
+ const v = node[k];
1157
+ if (Array.isArray(v)) v.forEach(visit);
1158
+ else if (typeof v === 'object' && v !== null) visit(v);
1159
+ }
1160
+ }
1161
+ };
1162
+ visit(program);
1163
+
1164
+ // ── ADC pin validity ────────────────────────────────────────────────────
1165
+ // The SAADC lowering resolves the adc.read argument to a channel via the
1166
+ // chip descriptor's adc.channels map (pin-first, then channel-number
1167
+ // fallback for the Arduino-compat ADC.read(n) form — see
1168
+ // adcChannelForPin). A number resolving to neither emits
1169
+ // __tc_adc-1_setup() — an undefined symbol → link error. Flag it at
1170
+ // compile time with a clear message instead of an opaque link failure.
1171
+ const adcPins = new Set((chip.adc?.channels ?? []).map((c) => c.pin));
1172
+ for (const pin of adcReadPins) {
1173
+ if (adcOverriddenPins.has(pin)) continue;
1174
+ if (adcChannelForPin(chip, pin) < 0) {
1175
+ const valid = [...adcPins].sort((x, y) => x - y).join(', ');
1176
+ // Cross-peripheral suggestion: the facts know what this pin IS wired
1177
+ // to — a PWM-capable pin misread as analog is the classic mix-up.
1178
+ const pwmOnPin = (chip.pwm?.specs ?? []).find((s) => s.pin === pin);
1179
+ const mixup = pwmOnPin
1180
+ ? ` GPIO ${pin} carries PWM on this board (${pwmOnPin.controller} ch ${pwmOnPin.channel}) — did you mean new PWM(${pin}, …)?`
1181
+ : '';
1182
+ diags.push({
1183
+ severity: 'error',
1184
+ code: 'zephyr-adc-pin-unavailable',
1185
+ message: `GPIO ${pin} is not a SAADC channel on ${chip.id} and cannot be read with adc.read.`,
1186
+ hint: valid
1187
+ ? `Use an analog-capable pin. On ${chip.id} (SAADC): ${valid}.${mixup}`
1188
+ : `This target has no ADC channels mapped in its chip descriptor.${mixup}`,
1189
+ source: program.fileName,
1190
+ });
1191
+ }
1192
+ }
1193
+
1194
+ // ── Interrupt pin validity ──────────────────────────────────────────────
1195
+ // GPIO.onInterrupt works on every REAL GPIO: pins listed in the chip
1196
+ // descriptor's gpio.interruptPins wire through the DT-spec chain, any
1197
+ // other in-range pin through the raw-controller chain. What cannot work
1198
+ // is a pin number no declared controller range covers (e.g. 99 on a
1199
+ // 34-pin SoC) — that lowering is a comment (silent no-op). Flag only
1200
+ // that case.
1201
+ const intPins = new Set((chip.gpio.interruptPins ?? []).map((p) => p.pin));
1202
+ for (const pin of interruptPins) {
1203
+ if (intPins.has(pin)) continue;
1204
+ const inRange = !chip.gpioControllers || chip.gpioControllers.length === 0
1205
+ || chip.gpioControllers.some((r) => pin >= r.minPin && pin <= r.maxPin);
1206
+ if (!inRange) {
1207
+ diags.push({
1208
+ severity: 'error',
1209
+ code: 'zephyr-interrupt-pin-unavailable',
1210
+ message: `GPIO ${pin} does not exist on ${chip.id}; onInterrupt is a no-op.`,
1211
+ hint: `Use a real GPIO on this board (controller ranges: ${chip.gpioControllers.map((r) => `${r.nodelabel} ${r.minPin}-${r.maxPin}`).join(', ')}).`,
1212
+ source: program.fileName,
1213
+ });
1214
+ }
1215
+ }
1216
+
1217
+ // ── DAC pin validity ────────────────────────────────────────────────────
1218
+ // dac.write resolves a HAL pin to a channel via the chip descriptor's
1219
+ // dac.channels map. A pin not in that map lowers to a comment (silent
1220
+ // no-op), and a chip without a `dac` entry (nRF52840, ESP32-S3) has no DAC
1221
+ // at all. Flag either case so the user gets a clear message instead of a
1222
+ // pin that silently does nothing.
1223
+ if (dacPins.size > 0) {
1224
+ const dacChannels = new Set((chip.dac?.channels ?? []).map((c) => c.pin));
1225
+ for (const pin of dacPins) {
1226
+ if (!dacChannels.has(pin)) {
1227
+ const adcCh = (chip.adc?.channels ?? []).find((c) => c.pin === pin);
1228
+ const mixup = adcCh
1229
+ ? ` GPIO ${pin} is an ADC channel (${adcCh.controller ?? chip.adc?.nodeLabel ?? 'adc'} ch ${adcCh.channel}) — did you mean new ADC(${pin})?`
1230
+ : '';
1231
+ diags.push({
1232
+ severity: 'error',
1233
+ code: 'zephyr-dac-pin-unavailable',
1234
+ message: `GPIO ${pin} is not a DAC channel on ${chip.id} and cannot be driven with dac.write.`,
1235
+ hint: dacChannels.size > 0
1236
+ ? `Use a DAC-capable pin. On ${chip.id}: ${[...dacChannels].sort((x, y) => x - y).join(', ')}.${mixup}`
1237
+ : `${chip.id} has no DAC. Use an esp32_devkitc target (ESP32 DAC on GPIO25/26).${mixup}`,
1238
+ source: program.fileName,
1239
+ });
1240
+ }
1241
+ }
1242
+ }
1243
+
1244
+ // ── PWM pin validity ────────────────────────────────────────────────────
1245
+ // pwm.write resolves a HAL pin to a DT spec via the chip descriptor's
1246
+ // pwm.specs (static) or pwm.matrix (ESP32 LEDC: any matrix pin synthesizes
1247
+ // a spec at build time). A pin on neither lowers to a comment — the pin
1248
+ // silently never toggles. Flag it so the user knows (warning, not error:
1249
+ // boards legitimately ship partial PWM coverage, e.g. only the aliased LED
1250
+ // channel, and the rest of the program still works).
1251
+ const pwmMatrix = chip.pwm?.matrix;
1252
+ const pwmSpecPins = new Set([
1253
+ ...(chip.pwm?.specs ?? []).map((s) => s.pin),
1254
+ ...(pwmMatrix?.pins ?? []),
1255
+ ]);
1256
+ const pwmValid = [...pwmSpecPins].sort((x, y) => x - y).join(', ');
1257
+ for (const pin of pwmPins) {
1258
+ if (pwmOverriddenPins.has(pin)) continue;
1259
+ if (!pwmSpecPins.has(pin)) {
1260
+ // Cross-peripheral suggestion: an analog pin driven as PWM is the
1261
+ // other classic mix-up.
1262
+ const adcCh = (chip.adc?.channels ?? []).find((c) => c.pin === pin);
1263
+ const mixup = adcCh
1264
+ ? ` GPIO ${pin} is an ADC channel (${adcCh.controller ?? chip.adc?.nodeLabel ?? 'adc'} ch ${adcCh.channel}) — did you mean new ADC(${pin})?`
1265
+ : '';
1266
+ diags.push({
1267
+ severity: 'warning',
1268
+ code: 'zephyr-pwm-pin-unavailable',
1269
+ message: pwmMatrix
1270
+ ? `pwm on GPIO ${pin} lowers to a no-op: the pin is outside ${chip.id}'s PWM-capable set (USB, flash/PSRAM, strapping, and console pads are excluded), so nothing is driven.`
1271
+ : `pwm on GPIO ${pin} lowers to a no-op: the pin has no PWM spec in ${chip.id}'s chip descriptor, so nothing is driven.`,
1272
+ hint: pwmValid
1273
+ ? pwmMatrix
1274
+ ? `PWM-capable pins on ${chip.id} (first ${pwmMatrix.channelCount} driven get channels): ${pwmValid}.${mixup}`
1275
+ : `PWM-capable pins on ${chip.id}: ${pwmValid}.${mixup}`
1276
+ : `${chip.id} maps no PWM channels in its chip descriptor — pwm.* are no-ops on this target.${mixup}`,
1277
+ source: program.fileName,
1278
+ });
1279
+ }
1280
+ }
1281
+ // ── Bus instance validity ───────────────────────────────────────────────
1282
+ // The bus lowerings resolve instance N against chip.<bus>.controllers[N]
1283
+ // and emit `__tc_<bus>N_dev` references; the state block is only declared
1284
+ // for declared controllers, so an out-of-range instance is an undefined
1285
+ // symbol at link time. Flag it here with the board's real controller set
1286
+ // instead (mirrors how the ADC gate replaces an opaque link error).
1287
+ const busLabels: Record<'i2c' | 'spi' | 'uart', { name: string; label: string }> = {
1288
+ i2c: { name: 'I2C', label: 'Wire' },
1289
+ spi: { name: 'SPI', label: 'SPI' },
1290
+ uart: { name: 'UART', label: 'Serial' },
1291
+ };
1292
+ for (const bus of ['i2c', 'spi', 'uart'] as const) {
1293
+ const controllers = chip[bus]?.controllers ?? [];
1294
+ for (const inst of busInstances[bus]) {
1295
+ if (inst < 0 || inst >= controllers.length) {
1296
+ diags.push({
1297
+ severity: 'error',
1298
+ code: 'zephyr-bus-instance-unavailable',
1299
+ message: `${busLabels[bus].name}${inst} is used but ${chip.id} declares only ${controllers.length} ${busLabels[bus].name} controller(s) in its Zephyr chip data — the generated code would not link.`,
1300
+ hint: controllers.length > 0
1301
+ ? `Use ${busLabels[bus].name}0 (the board's default-enabled controller: ${controllers.map((c) => c.nodeLabel).join(', ')}).`
1302
+ : `${chip.id} declares no ${busLabels[bus].name} controllers for Zephyr.`,
1303
+ source: program.fileName,
1304
+ });
1305
+ }
1306
+ }
1307
+ }
1308
+
1309
+ // ── Hardware-timer instance validity ────────────────────────────────────
1310
+ // hwtimer.* resolves the instance index to a counter device via the chip
1311
+ // descriptor's hwtimer.controllers. A chip without that entry (or an
1312
+ // out-of-range instance) lowers to a comment — flag it so the user knows
1313
+ // the timer will never fire.
1314
+ if (hwtimerInstances.size > 0) {
1315
+ const controllerCount = chip.hwtimer?.controllers.length ?? 0;
1316
+ for (const inst of hwtimerInstances) {
1317
+ if (controllerCount === 0) {
1318
+ diags.push({
1319
+ severity: 'error',
1320
+ code: 'zephyr-hwtimer-unavailable',
1321
+ message: `Hardware timer instance ${inst} is used but ${chip.id} exposes no free counter device.`,
1322
+ hint: `${chip.id} declares no hwtimer.controllers. Use a target with a free counter (e.g. nRF RTC1).`,
1323
+ source: program.fileName,
1324
+ });
1325
+ } else if (inst < 0 || inst >= controllerCount) {
1326
+ diags.push({
1327
+ severity: 'error',
1328
+ code: 'zephyr-hwtimer-instance-out-of-range',
1329
+ message: `Hardware timer instance ${inst} is out of range on ${chip.id} (0..${controllerCount - 1}).`,
1330
+ source: program.fileName,
1331
+ });
1332
+ }
1333
+ }
1334
+ }
1335
+
1336
+ // ── Watchdog availability ───────────────────────────────────────────────
1337
+ // wdt.* resolves the device via the chip descriptor's wdt.nodeLabel. A
1338
+ // chip without that entry (e.g. SAM D21 — Zephyr's samd21 dtsi exposes
1339
+ // no watchdog node) lowers to a comment — flag it so the user knows the
1340
+ // watchdog never arms.
1341
+ if (wdtOps.size > 0 && !chip.wdt) {
1342
+ diags.push({
1343
+ severity: 'error',
1344
+ code: 'zephyr-wdt-unavailable',
1345
+ message: `Watchdog ops are used but ${chip.id} exposes no watchdog device.`,
1346
+ hint: `${chip.id} declares no wdt.nodeLabel (Zephyr's devicetree for this SoC has no watchdog node). Use a target with a watchdog, or drop the wdt.* calls.`,
1347
+ source: program.fileName,
1348
+ });
1349
+ }
1350
+
1351
+ // ── Radio presence: family-derived, not board-curated ───────────────────
1352
+ // Espressif ESP32 variants carry a 2.4GHz WiFi radio; every other Zephyr
1353
+ // family in the catalog is radioless (until an Ethernet/board-wifi fact
1354
+ // exists). Derived from the soc name — or, when no board module was
1355
+ // generated, from the raw build target the config carries.
1356
+ const familyTarget = `${chip.soc || ''} ${(ctx?.frameworkData as Record<string, unknown> | undefined)?.target ?? ''} ${(ctx?.frameworkData as Record<string, unknown> | undefined)?.buildTarget ?? ''}`.toLowerCase();
1357
+ const wifiSupported = chip.wifi?.supported
1358
+ ?? familyTarget.split(/[^a-z0-9]+/).some((t) => t.startsWith('esp32'));
1359
+
1360
+ // ── WiFi target validity ────────────────────────────────────────────────
1361
+ // WiFi ops require a chip with a WiFi radio. The ESP32-S3 descriptor sets
1362
+ // wifi.supported; the XIAO nRF52840 omits it (no radio). Flag wifi usage on
1363
+ // a radioless chip so the user gets a clear "use an ESP32 target" message
1364
+ // instead of an opaque link/DT failure.
1365
+ if (usesWifiOps && !wifiSupported) {
1366
+ diags.push({
1367
+ severity: 'error',
1368
+ code: 'zephyr-wifi-unavailable-on-target',
1369
+ message: `WiFi ops are used but ${chip.id} has no WiFi radio.`,
1370
+ hint: `Use an esp32s3_devkitc or esp32_devkitc target (Espressif ESP32 variants have a 2.4GHz WiFi radio).`,
1371
+ source: program.fileName,
1372
+ });
1373
+ }
1374
+
1375
+ // ── HTTP target validity ──────────────────────────────────────────────
1376
+ // HTTP needs a network transport. On Zephyr the only networked target is
1377
+ // the ESP32 (WiFi) — the nRF52840 has neither WiFi nor Ethernet wired in
1378
+ // its chip descriptor, so the shim's socket/connect calls would fail at
1379
+ // runtime. Flag http usage on a chip without a network radio so the user
1380
+ // gets a clear "use an ESP32 target" message instead of an opaque link or
1381
+ // runtime failure. (HTTP rides over WiFi here; an Ethernet target would
1382
+ // set wifi.supported via a different transport flag if/when added.)
1383
+ if (usesHttpOps && !wifiSupported) {
1384
+ diags.push({
1385
+ severity: 'error',
1386
+ code: 'zephyr-http-unavailable-on-target',
1387
+ message: `HTTP ops are used but ${chip.id} has no network stack available.`,
1388
+ hint: `Use an esp32s3_devkitc or esp32_devkitc target (HTTP needs a network transport; the ESP32 WiFi radio provides it).`,
1389
+ source: program.fileName,
1390
+ });
1391
+ }
1392
+
1393
+ // ── MQTT target validity ─────────────────────────────────────────────
1394
+ // Same constraint as HTTP: MQTT needs a network transport to reach a broker.
1395
+ // Flag mqtt usage on a radioless chip so the user picks a networked target.
1396
+ if (usesMqttOps && !wifiSupported) {
1397
+ diags.push({
1398
+ severity: 'error',
1399
+ code: 'zephyr-mqtt-unavailable-on-target',
1400
+ message: `MQTT ops are used but ${chip.id} has no network stack available.`,
1401
+ hint: `Use an esp32s3_devkitc or esp32_devkitc target (MQTT needs a network transport; the ESP32 WiFi radio provides it).`,
1402
+ source: program.fileName,
1403
+ });
1404
+ }
1405
+
1406
+ // ── Unused-analysis: surface a hint that this is a no-analysis probe ────
1407
+ // (intentionally minimal — esp32 has richer rules around strapping/RTC pins;
1408
+ // nRF52840 has fewer silicon foot-guns, so the rules above are the material
1409
+ // ones. Extend as constraints are identified.)
1410
+
1411
+ return diags;
1412
+ }
1413
+
1414
+ /**
1415
+ * Strip Arduino library headers that @typecad/hal's class surfaces register
1416
+ * (I2CBus → <Wire.h>, SPIBus → <SPI.h>). The Zephyr
1417
+ * equivalents are usage-gated forcedIncludes (<zephyr/drivers/i2c.h>, …),
1418
+ * so an unstripped Wire.h reaches the west build and gcc rejects it
1419
+ * ("Wire.h: No such file or directory") — this hook is why I2C/SPI class
1420
+ * usage on Zephyr used to fail at compile time.
1421
+ */
1422
+ filterRequiredIncludes(includes: string[]): string[] {
1423
+ // Arduino-core / AVR-only headers the HAL classes declare via __includes.
1424
+ // On Zephyr the watchdog lowers to the Zephyr driver (<zephyr/drivers/
1425
+ // watchdog.h>, forced under usesWDT) — <avr/wdt.h> does not exist there.
1426
+ const arduinoOnlyHeaders = new Set(['<Wire.h>', '<SPI.h>', '<avr/wdt.h>', '<avr/io.h>', '<avr/interrupt.h>', '<avr/sleep.h>', '<avr/power.h>']);
1427
+ return includes.filter((i) => !arduinoOnlyHeaders.has(i));
1428
+ }
1429
+
1430
+ /**
1431
+ * Nothing to exclude: no shim or Zephyr core header forward-declares a user
1432
+ * function extern (the old setup()/loop() bridge was the only consumer of
1433
+ * this hook, excluding `loop`). The entrypoint itself (`main`) is already
1434
+ * excluded from the static forward-declaration path because it equals
1435
+ * entrypointFunctionName().
1436
+ */
1437
+ forwardDeclarationExclusions(): string[] {
1438
+ return [];
1439
+ }
1440
+
1441
+ // ── File shape ──────────────────────────────────────────────────────────
1442
+
1443
+ sourceExtension(): string {
1444
+ return 'cpp';
1445
+ }
1446
+
1447
+ entrypointFunctionName(): string {
1448
+ return 'main';
1449
+ }
1450
+
1451
+ requiresLoopFunction(): boolean {
1452
+ return false;
1453
+ }
1454
+
1455
+ overrideBaseName(
1456
+ originalBaseName: string,
1457
+ outDirBaseName: string,
1458
+ isEntryFile: boolean,
1459
+ isNpmPackage: boolean,
1460
+ ): string {
1461
+ // npm packages are library-style — don't rename. Entry files (non-npm) take
1462
+ // the out-dir name (mirrors the directory-name-must-match-entry rule).
1463
+ // Everything else passes through. (The manifest's entrypoint.overrideBaseName
1464
+ // field is dead — never read in src/ — so this method is the sole name source.)
1465
+ if (isNpmPackage) return originalBaseName;
1466
+ if (isEntryFile) return outDirBaseName;
1467
+ return originalBaseName;
1468
+ }
1469
+
1470
+ effectiveEmitMode(requestedMode: string, _isNpmPackage: boolean): string {
1471
+ // Zephyr always emits .cpp (no single-file entry equivalent to force away
1472
+ // from), so this is passthrough regardless of npm/app. The 2-param shape
1473
+ // interface and Arduino; behavior is identical across branches.
1474
+ return requestedMode;
1475
+ }
1476
+
1477
+ // ── Type normalisation ──────────────────────────────────────────────────
1478
+
1479
+ normalizeCppType(typeName: string): string {
1480
+ if (typeName === 'auto') return 'auto';
1481
+ if (typeName === 'std::string') return 'const char*';
1482
+ return typeName;
1483
+ }
1484
+
1485
+ defaultNumericType(compliance?: { isBanned(ruleId: string): boolean }): string {
1486
+ // Zephyr favors fixed-width types. Under AUTOSAR compliance (A3-9-1) emit
1487
+ // int32_t; otherwise the standard 'int'.
1488
+ return compliance?.isBanned('A3-9-1') ? 'int32_t' : 'int';
1489
+ }
1490
+
1491
+ mapReturnType(functionName: string, returnType: string): string {
1492
+ if (functionName === 'main') return 'int';
1493
+ return this.normalizeCppType(returnType);
1494
+ }
1495
+
1496
+ isStringLikeType(cppType: string): boolean {
1497
+ return cppType === 'std::string' || cppType === 'const char*' || cppType === 'char*';
1498
+ }
1499
+
1500
+ isPointerType(cppType: string): boolean {
1501
+ return cppType.endsWith('*');
1502
+ }
1503
+
1504
+ mapFunctionName(originalName: string): string {
1505
+ if (originalName === '__cuttlefish_entrypoint__') return 'main';
1506
+ return originalName;
1507
+ }
1508
+
1509
+ // ── Expression rendering ────────────────────────────────────────────────
1510
+
1511
+ normalizeRawExpression(value: string): string {
1512
+ let prev = '';
1513
+ let v = value;
1514
+ while (prev !== v) {
1515
+ prev = v;
1516
+ v = v.replace(/\bundefined\b/g, 'CUTTLEFISH_UNDEFINED');
1517
+ v = v.replace(/\bnull\b/g, 'CUTTLEFISH_UNDEFINED');
1518
+ }
1519
+ // String-method lowering is shared across targets (see string-method-
1520
+ // registry). Zephyr's strings are const char*, so the __tc_* helpers this
1521
+ // rewrites to (defined by the string_methods polyfill) take const char*.
1522
+ // includes/startsWith lower to inline strstr/strncmp (matching framework-
1523
+ // arduino); everything else → a __tc_* helper call.
1524
+ v = applyStringMethodRewrites(v, {
1525
+ wrapReceiverFor: new Set(['indexOf']),
1526
+ special: {
1527
+ includes: (recv, args) => `(strstr(${recv}, ${args[0]}) != NULL)`,
1528
+ startsWith: (recv, args) => `(strncmp(${recv}, ${args[0]}, strlen(${args[0]})) == 0)`,
1529
+ },
1530
+ });
1531
+ return v;
1532
+ }
1533
+
1534
+ nullValue(): string {
1535
+ return 'CUTTLEFISH_UNDEFINED';
1536
+ }
1537
+
1538
+ wrapStringConcat(): string | undefined {
1539
+ return undefined;
1540
+ }
1541
+
1542
+ wrapStringObject(value: string): string {
1543
+ return `std::to_string(${value})`;
1544
+ }
1545
+
1546
+ useSnprintfForStrings(): boolean {
1547
+ return true;
1548
+ }
1549
+
1550
+ renameEnumMember(_enumName: string, memberName: string): string {
1551
+ return memberName;
1552
+ }
1553
+
1554
+ private _largeEnumNames = new Set<string>();
1555
+
1556
+ setLargeEnumNames(names: ReadonlySet<string>): void {
1557
+ this._largeEnumNames = new Set(names);
1558
+ }
1559
+
1560
+ enumCastType(_enumName: string): string | undefined {
1561
+ return undefined;
1562
+ }
1563
+
1564
+ renderBoardDefinitionAccess(
1565
+ chain: string[],
1566
+ boardConstants?: BoardConstants,
1567
+ ): string | undefined {
1568
+ // Fold Board.definition.<path> / Pins.definition.<path> into the literal
1569
+ // board-constant value, mirroring framework-arduino. The renderer calls
1570
+ // this (expression-renderer.ts) with the populated boardConstants from the
1571
+ // loaded board/MCU package, so a known path resolves to its scalar value.
1572
+ // `board.resolve` HAL ops, by contrast, are constant-folded earlier at
1573
+ // IR-build time and never reach here; see lowering/board.ts.
1574
+ if (chain.length < 3) return undefined;
1575
+ if (chain[0] !== 'Board' && chain[0] !== 'Pins') return undefined;
1576
+ if (chain[1] !== 'definition') return undefined;
1577
+ if (!boardConstants) return undefined;
1578
+ const path = chain.slice(2).join('.');
1579
+ const val = boardConstants.get(path);
1580
+ return val !== undefined ? String(val) : undefined;
1581
+ }
1582
+
1583
+ // ── Statement rendering ─────────────────────────────────────────────────
1584
+
1585
+ promotesArrayLiteralsToStaticArray(): boolean {
1586
+ // No std::vector in the minimal C++ lib — promote array literals to the
1587
+ // StaticArray wrapper (the embedded/generic default).
1588
+ return true;
1589
+ }
1590
+
1591
+ renderThrow(_valueExpr: string): string {
1592
+ // Zephyr minimal config disables C++ exceptions (CONFIG_CPP_EXCEPTIONS=n).
1593
+ // A throw becomes an infinite halt loop instead of a real throw.
1594
+ return 'for (;;) { k_msleep(1000); }';
1595
+ }
1596
+
1597
+ objectFieldInitializer(): string | undefined {
1598
+ return undefined;
1599
+ }
1600
+
1601
+ overrideClassFieldType(_fieldName: string, normalizedType: string): string {
1602
+ return normalizedType;
1603
+ }
1604
+
1605
+ // ── Name guards ─────────────────────────────────────────────────────────
1606
+
1607
+ reservedNames(): ReadonlySet<string> {
1608
+ return new Set<string>();
1609
+ }
1610
+
1611
+ passthroughMacroNames(): ReadonlySet<string> {
1612
+ return new Set<string>();
1613
+ }
1614
+
1615
+ apiReservedEnumNames(): ReadonlySet<string> {
1616
+ return new Set<string>();
1617
+ }
1618
+
1619
+ apiReservedEnumGuard(): string {
1620
+ return '';
1621
+ }
1622
+
1623
+ // ── Interrupt safety ─────────────────────────────────────────────────────
1624
+ // Zephyr ISRs run above thread context: anything that sleeps (k_msleep),
1625
+ // pends, or takes a driver lock is illegal there (asserted by the kernel in
1626
+ // debug builds; corrupts scheduler state otherwise). The keys below are
1627
+ // matched by cuttlefish's interrupt-analysis pass in two shapes: bare/qualified
1628
+ // callee names (call statements) and resolved hal-op payloads — the analyzer
1629
+ // maps timing.delay/delay_microseconds/sleep/busy_wait_us ops back to the
1630
+ // bare timing keys, and matches the op payload's port/bus field (UART0,
1631
+ // I2C0, SPI0, …) against these keys directly. The same keys ArduinoStrategy
1632
+ // uses.
1633
+ isrUnsafeOperations(): Map<string, { reason: string; severity: 'warning' | 'info' }> {
1634
+ return new Map<string, { reason: string; severity: 'warning' | 'info' }>([
1635
+ ['delay', {
1636
+ reason: 'delay() lowers to k_msleep(), which sleeps the calling thread — illegal in Zephyr interrupt context (submit a k_work item or arm a k_timer instead)',
1637
+ severity: 'warning',
1638
+ }],
1639
+ ['delayMicroseconds', {
1640
+ reason: 'delayMicroseconds() busy-waits the CPU for the full delay, stalling every lower-priority interrupt and the scheduler for its duration',
1641
+ severity: 'warning',
1642
+ }],
1643
+ ['I2C0', {
1644
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
1645
+ severity: 'warning',
1646
+ }],
1647
+ ['I2C1', {
1648
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
1649
+ severity: 'warning',
1650
+ }],
1651
+ ['SPI0', {
1652
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
1653
+ severity: 'warning',
1654
+ }],
1655
+ ['SPI1', {
1656
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
1657
+ severity: 'warning',
1658
+ }],
1659
+ ['UART0', {
1660
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
1661
+ severity: 'info',
1662
+ }],
1663
+ ['UART1', {
1664
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
1665
+ severity: 'info',
1666
+ }],
1667
+ ]);
1668
+ }
1669
+
1670
+ ambientTypeDeclarations(): string[] {
1671
+ // Preferences is the only HAL surface the framework lowers that is used as
1672
+ // a bare global (the HAL Preferences class is exported, but the canonical
1673
+ // usage — and the hal/tests/14-preferences hardware suite — references it
1674
+ // as an unqualified `Preferences.*`). Declaring it ambient lets those
1675
+ // programs type-check and resolve to the preferences.* ops the lowering
1676
+ // in src/lowering/preferences.ts handles (ZMS-backed settings). Mirrors
1677
+ // framework-arduino's ambient Preferences declaration.
1678
+ return [
1679
+ "",
1680
+ " // Persistent key/value store (ZMS-backed Zephyr settings — see",
1681
+ " // src/lowering/preferences.ts). begin/end carry the namespace prefix;",
1682
+ " // typed put/get round-trip through an in-RAM cache + settings_save_one.",
1683
+ " const Preferences: {",
1684
+ " begin(name: string, readOnly?: boolean): void;",
1685
+ " end(): void;",
1686
+ " clear(): void;",
1687
+ " putInt(key: string, value: number): void;",
1688
+ " getInt(key: string, defaultValue: number): number;",
1689
+ " putUInt(key: string, value: number): void;",
1690
+ " getUInt(key: string, defaultValue: number): number;",
1691
+ " putBool(key: string, value: boolean): void;",
1692
+ " getBool(key: string, defaultValue: boolean): boolean;",
1693
+ " putFloat(key: string, value: number): void;",
1694
+ " getFloat(key: string, defaultValue: number): number;",
1695
+ " putString(key: string, value: string): void;",
1696
+ " getString(key: string, defaultValue: string): string;",
1697
+ " remove(key: string): void;",
1698
+ " };",
1699
+ ];
1700
+ }
1701
+
1702
+ // ── Includes ────────────────────────────────────────────────────────────
1703
+
1704
+ needsIostream(): boolean {
1705
+ return false;
1706
+ }
1707
+
1708
+ needsStdString(): boolean {
1709
+ // Zephyr's minimal C++ lib has no <string>. Set false so the transpiler
1710
+ // does not emit std::string-dependent code (e.g. the string-method
1711
+ // polyfills). A program that needs it must enable a full STL.
1712
+ return false;
1713
+ }
1714
+
1715
+ needsStdVector(): boolean {
1716
+ // No <vector> in the minimal C++ lib.
1717
+ return false;
1718
+ }
1719
+
1720
+ needsStdExcept(): boolean {
1721
+ return false;
1722
+ }
1723
+
1724
+ needsStdFunction(): boolean {
1725
+ // No <functional> in the minimal C++ lib.
1726
+ return false;
1727
+ }
1728
+
1729
+ mathHeader(): string {
1730
+ // <cmath>, not <math.h>: the shared lowering emits Math.<fn>() as
1731
+ // std::<fn>() (expression-to-ir), and picolibc's <math.h> declares the
1732
+ // C names in the global namespace only — std::round et al. fail to
1733
+ // compile. Builds run with CONFIG_REQUIRES_FULL_LIBCPP (real libstdc++),
1734
+ // whose <cmath> provides the std:: names.
1735
+ return '<cmath>';
1736
+ }
1737
+
1738
+ cstringHeader(): string {
1739
+ return '<cstring>';
1740
+ }
1741
+
1742
+ needsLargeEnumUnderlying(): boolean {
1743
+ return false;
1744
+ }
1745
+
1746
+ // ── Struct field handling ───────────────────────────────────────────────
1747
+
1748
+ renameStructField(fieldName: string): string {
1749
+ return fieldName;
1750
+ }
1751
+
1752
+ structFieldInitializer(): string | undefined {
1753
+ return undefined;
1754
+ }
1755
+
1756
+ // ── Async ───────────────────────────────────────────────────────────────
1757
+ // Promises use the heap-free static runtime (generateStaticAsyncRuntime),
1758
+ // pumped cooperatively via cuttlefish_pump_microtasks(). There is no
1759
+ // __tc_timer_runtime.run() poll — periodic work is a Thread (k_thread) or a
1760
+ // Counter (hardware timer), never a cooperative timer queue.
1761
+
1762
+ getAsyncRuntimeConfig(): AsyncRuntimeConfig {
1763
+ return {
1764
+ queueCapacity: 64,
1765
+ scheduler: 'microtask',
1766
+ waitForPinEdge: 'stub',
1767
+ hasPromiseRuntime: true,
1768
+ hasTimers: false,
1769
+ // Static (heap-free) runtime — no STL headers required.
1770
+ requiredIncludes: [],
1771
+ };
1772
+ }
1773
+
1774
+ /**
1775
+ * Board module generation: join the Zephyr board data pack (extracted
1776
+ * from the pinned tree) with the curated soc descriptors. See
1777
+ * src/boardgen.ts.
1778
+ */
1779
+ generateBoardModule(
1780
+ target: string,
1781
+ opts?: { factsJson?: string; asBuiltJson?: string },
1782
+ ): { boardTs: string; boardJson: string; warnings?: readonly string[] } | undefined {
1783
+ try {
1784
+ const g = generateBoard(target, { factsJson: opts?.factsJson, asBuiltJson: opts?.asBuiltJson });
1785
+ return { boardTs: g.boardTs, boardJson: g.boardJson, ...(g.warnings ? { warnings: g.warnings } : {}) };
1786
+ } catch (err) {
1787
+ // A malformed facts/as-built file is the USER's error — surface it
1788
+ // verbatim instead of the generic "cannot generate" below.
1789
+ if (err instanceof Error && (err.message.includes('typecad-hal.facts.json') || err.message.includes('as-built.json'))) throw err;
1790
+ return undefined;
1791
+ }
1792
+ }
1793
+
1794
+ /**
1795
+ * Regenerate the board catalog overlay from the user's own Zephyr tree —
1796
+ * `typecad-hal board sync`. After a `west update`, this is how new/changed/
1797
+ * removed boards reach projects without a cuttlefish release. See
1798
+ * src/sdk/board-catalog-sync.ts.
1799
+ */
1800
+ syncBoardCatalog(zephyrBase?: string): BoardCatalogSyncReport {
1801
+ return syncBoardCatalog(zephyrBase ? { zephyrBase } : {});
1802
+ }
1803
+
1804
+ /**
1805
+ * Refresh the overlay only when it is stale (provenance no longer matches
1806
+ * the tree) — the pre-step `typecad-hal board regen` runs so a regen after
1807
+ * `west update` picks up the tree's boards automatically. Tree walk only
1808
+ * happens when there is actual refreshing to do.
1809
+ */
1810
+ ensureFreshBoardCatalog(): BoardCatalogEnsureResult {
1811
+ return ensureFreshBoardCatalog();
1812
+ }
1813
+
1814
+ asyncLoopInjection(taskVarNames: string[], config: AsyncRuntimeConfig): string[];
1815
+ asyncLoopInjection(taskVarNames: string[], hasPromiseRuntime: boolean, hasTimers: boolean): string[];
1816
+ asyncLoopInjection(
1817
+ taskVarNames: string[],
1818
+ configOrBool: AsyncRuntimeConfig | boolean,
1819
+ _hasTimers?: boolean,
1820
+ ): string[] {
1821
+ const cfg =
1822
+ typeof configOrBool === 'boolean'
1823
+ ? { hasPromiseRuntime: configOrBool, hasTimers: _hasTimers ?? false }
1824
+ : configOrBool;
1825
+ // Drive every async state-machine task once per scheduler iteration. The
1826
+ // task globals auto-start on their first .run() (constructor sets STATE_0,
1827
+ // which runs unconditionally), so this is both the start and the per-frame
1828
+ // advance. The state machine no-ops in its terminal/cyclic state, so
1829
+ // unconditional .run() is correct (mirrors framework-arduino). No
1830
+ // isComplete() gating.
1831
+ const work: string[] = [];
1832
+ if (cfg.hasPromiseRuntime) {
1833
+ work.push('cuttlefish_pump_microtasks();');
1834
+ }
1835
+ for (const n of taskVarNames) {
1836
+ work.push(`${n}.run();`);
1837
+ }
1838
+ // NOTE: no timer-queue poll — periodic work is a Thread (k_thread) or a
1839
+ // Counter (hardware timer), never a cooperative poll.
1840
+ //
1841
+ // main() runs once, so the per-frame work must close over its own loop.
1842
+ // Under a mounted UI the emitter's hostEventLoop() already wraps ui_tick +
1843
+ // these lines in `while (...) { ...; k_msleep(1); }` — return flat lines
1844
+ // there. Otherwise (async/timer programs with no UI) self-wrap the same
1845
+ // scheduler loop here. For a program whose top-level `while (true)` never
1846
+ // exits, the loop below it is unreachable — harmless, and identical to the
1847
+ // old bridge behavior (whose for(;;) around loop() was equally dead).
1848
+ if (entryHasUI()) {
1849
+ return work;
1850
+ }
1851
+ return ['for (;;) {', ...work.map((l) => ` ${l}`), ' k_msleep(1);', '}'];
1852
+ }
1853
+
1854
+ asyncDriverFunctionName(): string {
1855
+ return 'main';
1856
+ }
1857
+
1858
+ /**
1859
+ * Zephyr's scheduler loop for event-driven programs (UI mounted). The
1860
+ * emitter wraps the per-frame work — ui_tick plus the asyncLoopInjection
1861
+ * lines — in `while (__tc_zephyr_event_loop) { ...; k_msleep(1); }`: a
1862
+ * 1 ms cooperative yield to the kernel each frame, matching the cadence the
1863
+ * old setup()/loop() bridge gave loop(). The flag is never cleared — the
1864
+ * loop is the program's lifetime — but keeping the bool gives the condition
1865
+ * a used symbol (a bare `while (true)` would leave nothing to name, and an
1866
+ * unused flag would trip -Werror's -Wunused-variable).
1867
+ */
1868
+ hostEventLoop(): {
1869
+ flagName: string;
1870
+ continueCondition: string;
1871
+ preIteration: string;
1872
+ postIteration: string;
1873
+ } | null {
1874
+ return {
1875
+ flagName: '__tc_zephyr_event_loop',
1876
+ continueCondition: '__tc_zephyr_event_loop',
1877
+ preIteration: '',
1878
+ postIteration: 'k_msleep(1);',
1879
+ };
1880
+ }
1881
+
1882
+ // ── Type aliases ────────────────────────────────────────────────────────
1883
+
1884
+ shouldSkipTypeAlias(): boolean {
1885
+ return false;
1886
+ }
1887
+
1888
+ // ── Diagnostics ─────────────────────────────────────────────────────────
1889
+
1890
+ emitDiagnostics(): Diagnostic[] {
1891
+ return [];
1892
+ }
1893
+
1894
+ currentTimeMillis(): string {
1895
+ return '__tc_now_ms()';
1896
+ }
1897
+
1898
+ // ── Build configuration ──────────────────────────────────────────────────
1899
+
1900
+ asyncQueueCapacity(): number {
1901
+ return 64;
1902
+ }
1903
+
1904
+ outputSubdirectory(_baseName: string): string {
1905
+ // Zephyr convention: application sources live under src/.
1906
+ return 'src';
1907
+ }
1908
+
1909
+ generateHeaderFile(): boolean {
1910
+ return true;
1911
+ }
1912
+
1913
+ enumApiGuard(_enumName: string): { open: string; close: string } | undefined {
1914
+ return undefined;
1915
+ }
1916
+
1917
+ getStdLibSupport(_architecture?: string): StdLibSupport {
1918
+ // Zephyr's minimal C++ support (lib/cpp/minimal) provides only <cstddef>,
1919
+ // <cstdint>, <new>. No <vector>, <string>, <iostream>, <functional>, no
1920
+ // exceptions, no RTTI. The blink MVP uses only GPIO + kernel timing, so
1921
+ // none of those are needed. Array/string literals are not promoted to the
1922
+ // STL containers; a future program needing them must enable a full STL
1923
+ // and update these flags.
1924
+ return {
1925
+ hasVector: false,
1926
+ hasString: false,
1927
+ hasIostream: false,
1928
+ hasExceptions: false,
1929
+ hasRTTI: false,
1930
+ recommendedArrayImpl: 'static_array',
1931
+ recommendedStringImpl: 'static_string',
1932
+ };
1933
+ }
1934
+
1935
+ // ── Polyfills ───────────────────────────────────────────────────────────
1936
+ // Zephyr is a no-STL target (hasVector/hasString = false), so array/string
1937
+ // literals lower to __tc_StaticArray / const char* and string methods lower
1938
+ // to __tc_* helpers — both need STL-free definitions emitted here (there is
1939
+ // no shared-runtime fallback; the pipeline sources 100% of polyfills from
1940
+ // generateNativePolyfills). Mirrors framework-arduino's AVR polyfills.
1941
+
1942
+ nativePolyfills(): Set<string> {
1943
+ // cuttlefish_halt: always (the runtime header may reference it).
1944
+ // string_methods / static_array: STL-free array + string helpers a no-STL
1945
+ // target needs (mutated/struct array literals + any string method).
1946
+ // async_runtime: heap-free static Promise/microtask runtime (no STL needed).
1947
+ return new Set<string>([
1948
+ 'cuttlefish_halt', 'wiring_compat', 'string_methods', 'static_array',
1949
+ 'async_runtime',
1950
+ ]);
1951
+ }
1952
+
1953
+ generateNativePolyfills(program?: ProgramIR, ctx?: PlatformContext): RuntimePolyfillIR[] {
1954
+ // wiring_compat (digitalRead/HIGH/LOW macros + the __tc_gpio_read forward
1955
+ // declaration) is emitted only when something reads a pin: user
1956
+ // digitalRead() calls, the @typecad/safety voter, or the UI runtime
1957
+ // header's unconditional digitalRead() poll (init-press-input.ts — the
1958
+ // loop body is dead when no pin watchers are configured but must
1959
+ // compile). needsGpioReadShim defaults to true without analysis so
1960
+ // capability queries keep seeing it.
1961
+ const wiringCompat: RuntimePolyfillIR = {
1962
+ // Wiring-compatibility shims for symbols the UI runtime header
1963
+ // references unconditionally (e.g. init-press-input.ts polls pin
1964
+ // watchers via digitalRead/HIGH/LOW even when none are configured —
1965
+ // the loop body is dead but must compile). Zephyr lowers GPIO through
1966
+ // its __tc_gpio_* helpers (defined in shimLines); these macros route
1967
+ // the Wiring tokens to them.
1968
+ kind: 'polyfill',
1969
+ id: 'wiring_compat',
1970
+ domain: 'standard' as const,
1971
+ requiredIncludes: [],
1972
+ forwardDeclarations: [
1973
+ // Forward-declared so the digitalRead macro (below) can reference it
1974
+ // before the shim block defines the body. The shim emits the full
1975
+ // definition via gpio_pin_get_raw.
1976
+ 'int __tc_gpio_read(int pin);',
1977
+ ],
1978
+ helperStructs: [],
1979
+ helperFunctions: [],
1980
+ shimMacros: [
1981
+ '#ifndef HIGH',
1982
+ '#define HIGH 1',
1983
+ '#endif',
1984
+ '#ifndef LOW',
1985
+ '#define LOW 0',
1986
+ '#endif',
1987
+ '#ifndef digitalRead',
1988
+ '#define digitalRead(pin) __tc_gpio_read(pin)',
1989
+ '#endif',
1990
+ ],
1991
+ dependencies: [],
1992
+ };
1993
+ const polyfills: RuntimePolyfillIR[] = [
1994
+ {
1995
+ kind: 'polyfill',
1996
+ id: 'cuttlefish_halt',
1997
+ domain: 'standard' as const,
1998
+ requiredIncludes: [],
1999
+ forwardDeclarations: [],
2000
+ helperStructs: [],
2001
+ helperFunctions: [
2002
+ '[[noreturn]] inline void cuttlefish_halt() { for (;;) { k_msleep(1000); } }',
2003
+ ],
2004
+ shimMacros: [],
2005
+ dependencies: [],
2006
+ },
2007
+ ...(this.needsGpioReadShim(program, ctx) ? [wiringCompat] : []),
2008
+ {
2009
+ // STL-free string-method polyfills. String methods (.toUpperCase(),
2010
+ // .includes(), .substring(), …) lower at IR level to __tc_* helpers for
2011
+ // every target; this supplies their definitions. Minimal-libc friendly:
2012
+ // only <cstring> primitives (no <cctype> — case conversion is inline
2013
+ // ASCII so the polyfill is self-contained). Mirrors framework-arduino.
2014
+ kind: 'polyfill',
2015
+ id: 'string_methods',
2016
+ domain: 'embedded' as const,
2017
+ requiredIncludes: ['<cstring>'],
2018
+ forwardDeclarations: [],
2019
+ helperStructs: [],
2020
+ helperFunctions: [`
2021
+ // TypeCAD string method polyfills (Zephyr, minimal-libc).
2022
+ #ifndef CUTTLEFISH_STR_BUF_SIZE
2023
+ #define CUTTLEFISH_STR_BUF_SIZE 64
2024
+ #endif
2025
+ bool __tc_endsWith(const char* s, const char* suffix) { int sl = strlen(s), tl = strlen(suffix); return sl >= tl && strcmp(s + sl - tl, suffix) == 0; }
2026
+ const char* __tc_toUpperCase(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; for (char* p = b; *p; p++) { if (*p >= 'a' && *p <= 'z') { *p = static_cast<char>(*p - 32); } } return b; }
2027
+ const char* __tc_toLowerCase(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; for (char* p = b; *p; p++) { if (*p >= 'A' && *p <= 'Z') { *p = static_cast<char>(*p + 32); } } return b; }
2028
+ const char* __tc_trim(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; while (*s == ' ' || *s == '\\t' || *s == '\\n' || *s == '\\r') s++; int len = strlen(s); while (len > 0 && (s[len-1] == ' ' || s[len-1] == '\\t' || s[len-1] == '\\n' || s[len-1] == '\\r')) len--; int cplen = len < CUTTLEFISH_STR_BUF_SIZE - 1 ? len : CUTTLEFISH_STR_BUF_SIZE - 1; strncpy(b, s, cplen); b[cplen] = '\\0'; return b; }
2029
+ const char* __tc_substring2(const char* s, int start, int end) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; int slen = strlen(s); if (start < 0) start = 0; if (end > slen) end = slen; if (end < start) end = start; int len = end - start; if (len >= CUTTLEFISH_STR_BUF_SIZE) len = CUTTLEFISH_STR_BUF_SIZE - 1; strncpy(b, s + start, len); b[len] = '\\0'; return b; }
2030
+ const char* __tc_substring1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
2031
+ const char* __tc_slice2(const char* s, int start, int end) { return __tc_substring2(s, start, end); }
2032
+ const char* __tc_slice1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
2033
+ const char* __tc_replace(const char* s, const char* old, const char* repl) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; const char* pos = strstr(s, old); if (!pos) { strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; return b; } int beforeLen = static_cast<int>(pos - s); int oldLen = static_cast<int>(strlen(old)); int replLen = static_cast<int>(strlen(repl)); if (beforeLen + replLen + static_cast<int>(strlen(pos + oldLen)) >= CUTTLEFISH_STR_BUF_SIZE) { strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; return b; } memcpy(b, s, beforeLen); memcpy(b + beforeLen, repl, replLen); strcpy(b + beforeLen + replLen, pos + oldLen); return b; }
2034
+ const char* __tc_charAt(const char* s, int idx) { static char buf[2][2]; static uint8_t slot = 0; slot ^= 1; buf[slot][0] = s[idx]; buf[slot][1] = '\\0'; return buf[slot]; }
2035
+ int __tc_charCodeAt(const char* s, int idx) { return static_cast<int>(static_cast<unsigned char>(s[idx])); }
2036
+ int __tc_indexOf(const char* s, const char* needle) { const char* p = strstr(s, needle); return p ? static_cast<int>(p - s) : -1; }
2037
+ `],
2038
+ shimMacros: [],
2039
+ dependencies: [],
2040
+ },
2041
+ {
2042
+ // STL-free fixed-size array wrapper. Mutated/struct-element array
2043
+ // literals and array methods (.push/.pop/.map/.filter) lower to
2044
+ // __tc_StaticArray<T,N>; this supplies the template. Idempotent guard
2045
+ // so a redefinition is a no-op. Mirrors framework-arduino.
2046
+ kind: 'polyfill',
2047
+ id: 'static_array',
2048
+ domain: 'embedded' as const,
2049
+ requiredIncludes: [],
2050
+ forwardDeclarations: [],
2051
+ helperStructs: [],
2052
+ helperFunctions: [`
2053
+ #ifndef __TC_STATIC_ARRAY_DEFINED
2054
+ #define __TC_STATIC_ARRAY_DEFINED
2055
+ template<typename T, int N>
2056
+ struct __tc_StaticArray {
2057
+ T data[N];
2058
+ int _size;
2059
+ __tc_StaticArray() : _size(0) {}
2060
+ int length() const { return _size; }
2061
+ int size() const { return _size; }
2062
+ void push(T val) { if (_size < N) data[_size++] = val; }
2063
+ T pop() { return (_size > 0) ? data[--_size] : T(); }
2064
+ int indexOf(T val) const { for (int i = 0; i < _size; i++) if (data[i] == val) return i; return -1; }
2065
+ T& operator[](int i) { return data[i]; }
2066
+ const T& operator[](int i) const { return data[i]; }
2067
+ T* begin() { return &data[0]; }
2068
+ T* end() { return &data[_size]; }
2069
+ const T* begin() const { return &data[0]; }
2070
+ const T* end() const { return &data[_size]; }
2071
+ };
2072
+ #endif
2073
+ `],
2074
+ shimMacros: [],
2075
+ dependencies: [],
2076
+ },
2077
+ ];
2078
+
2079
+ // async_runtime — heap-free static Promise/microtask runtime. Emitted when
2080
+ // the program declares an async function OR references an async-runtime
2081
+ // symbol (Async.sleep/.then from a non-async fn). The static path requires
2082
+ // no STL headers, so it is safe under Zephyr's minimal C++ lib.
2083
+ const usesAsync = !!program
2084
+ && (program.functions.some((fn: any) => fn && fn.isAsync) || this.programUsesAsyncRuntime(program));
2085
+ if (usesAsync) {
2086
+ polyfills.push({
2087
+ kind: 'polyfill',
2088
+ id: 'async_runtime',
2089
+ domain: 'embedded',
2090
+ requiredIncludes: [],
2091
+ // Polyfill definitions emit before shimLines, but the runtime's
2092
+ // timer bodies call __tc_now_ms() (defined in shimLines) — declare
2093
+ // it first so the polyfill compiles even for programs whose source
2094
+ // has no explicit timing call.
2095
+ forwardDeclarations: ['uint32_t __tc_now_ms(void);'],
2096
+ helperStructs: [generateStaticAsyncRuntime(8, this.getAsyncRuntimeConfig().waitForPinEdge)],
2097
+ helperFunctions: [],
2098
+ shimMacros: [],
2099
+ dependencies: [],
2100
+ hasPromiseRuntime: true,
2101
+ } as RuntimePolyfillIR);
2102
+ }
2103
+ return polyfills;
2104
+ }
2105
+
2106
+ // ── HAL ──────────────────────────────────────────────────────────────────
2107
+
2108
+ resolveHALOperation(op: HALOpIR): { code?: string; expression?: string } | undefined {
2109
+ return lowerHalOp(op);
2110
+ }
2111
+
2112
+ modelsGpio(): boolean {
2113
+ return true;
2114
+ }
2115
+
2116
+ // ── Atomic HAL primitives ─────────────────────────────────────────────────
2117
+ // Zephyr lowers GPIO through devicetree specs and its own __tc_gpio_* helpers
2118
+ // (defined in shimLines via gpio_pin_get_raw / gpio_pin_set_raw). Cuttlefish
2119
+ // asks these instead of emitting Wiring tokens by name. The async polling
2120
+ // path is gated to 'stub' on Zephyr (waitForPinEdge), so delayMs is unlikely
2121
+ // to be called here, but a busy-wait form is provided for completeness.
2122
+ readDigitalPin(pin: string): string {
2123
+ return `__tc_gpio_read(${pin})`;
2124
+ }
2125
+ readAnalogPin(pin: string): string {
2126
+ // Zephyr ADC is lowered through its own shim; this stub keeps cuttlefish
2127
+ // from emitting a Wiring analogRead token. Update if a __tc_adc_read helper
2128
+ // is introduced.
2129
+ return `/* adc lowering via zephyr shim */ 0`;
2130
+ }
2131
+ writeDigitalPin(pin: string, val: string): string {
2132
+ return `__tc_gpio_write(${pin}, ${val})`;
2133
+ }
2134
+ setPinMode(_pin: string, _mode: string): string {
2135
+ // Zephyr configures pin direction via devicetree, not a runtime pinMode.
2136
+ return `/* pin mode configured via devicetree */`;
2137
+ }
2138
+ delayMs(ms: string): string {
2139
+ return `k_msleep(${ms})`;
2140
+ }
2141
+ delayMicroseconds(us: string): string {
2142
+ return `__tc_delay_us(${us})`;
2143
+ }
2144
+ halCallNames(): ReadonlySet<string> {
2145
+ // Zephyr's HAL surface uses __tc_ prefixed helpers + the Zephyr API.
2146
+ return new Set<string>([
2147
+ "__tc_gpio_read", "__tc_gpio_write", "__tc_delay_us",
2148
+ "gpio_pin_get_raw", "gpio_pin_set_raw", "k_msleep", "k_busy_wait",
2149
+ ]);
2150
+ }
2151
+ isHalCall(name: string): boolean {
2152
+ return this.halCallNames().has(name);
2153
+ }
2154
+ analogReadCallNames(): ReadonlySet<string> {
2155
+ return new Set<string>();
2156
+ }
2157
+
2158
+ // ── RTOS ─────────────────────────────────────────────────────────────────
2159
+
2160
+ isRtosTarget(): boolean {
2161
+ // Zephyr is a preemptive RTOS — delay()/k_msleep inside main() is the
2162
+ // expected cooperative yield, not an anti-pattern to warn about.
2163
+ return true;
2164
+ }
2165
+
2166
+ // Display state (mirrors Arduino's _displayCtx). Seeded on display.init; the
2167
+ // validator-probe path seeds the default profile lazily.
2168
+ private _displayState: DisplayState = newDisplayState();
2169
+
2170
+ // ── Graphics ──────────────────────────────────────────────────────────────
2171
+ // Generic <zephyr/drivers/display.h> + ported GFX primitives (see src/display/).
2172
+ // resolveDisplayOp delegates to resolveZephyrDisplayOp with the per-build
2173
+ // DisplayState; the GFX runtime (device handle + line buffer + helpers) is
2174
+ // emitted into shimLines when usesDisplay.
2175
+
2176
+ resolveDisplayOp(op: DisplayHALOp): { code?: string; expression?: string } | undefined {
2177
+ return resolveZephyrDisplayOp(op, this._displayState);
2178
+ }
2179
+
2180
+ supportedDisplayDrivers(): ReadonlySet<string> {
2181
+ return new Set<string>(Object.keys(ZEPHYR_DISPLAY_PROFILES));
2182
+ }
2183
+
2184
+ // ── Strategy-owned display/touch adapter seam ────────────────────────────
2185
+ // Zephyr owns its display + touch adapters: the UI display adapter bridges
2186
+ // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
2187
+ // format, see src/display/ui-adapter.ts), and the touch adapters drive the
2188
+ // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
2189
+ // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
2190
+ // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
2191
+ // knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
2192
+
2193
+ providesDisplayAdapter(): boolean { return true; }
2194
+
2195
+ resolveDisplayAdapter(display: ResolvedDisplay): DisplayAdapterCode | undefined {
2196
+ const code = zephyrDisplayAdapterGenerator(display);
2197
+ return code ?? undefined;
2198
+ }
2199
+
2200
+ providesTouchAdapter(): boolean { return true; }
2201
+
2202
+ resolveTouchAdapter(touch: TouchProfile): TouchAdapterCodegen | undefined {
2203
+ return zephyrTouchAdapter(touch);
2204
+ }
2205
+
2206
+ // Named display-profile registry: maps config `profile` values (e.g.
2207
+ // "st7796-zephyr") to the shared DisplayProfile shape so transpile.ts can
2208
+ // resolve them per-framework. The Zephyr profiles are DT-binding descriptors;
2209
+ // BUILT_IN_PROFILES (display/profiles.ts) is the single DT-binding →
2210
+ // shared-shape mapping, shared with the preview's registry loader.
2211
+ getProfileRegistry(): Map<string, DisplayProfile> {
2212
+ return new Map(Object.entries(BUILT_IN_PROFILES));
2213
+ }
2214
+
2215
+ colorFormat(): 'rgb565' | 'rgb666' | 'rgb888' | 'mono' {
2216
+ return 'rgb565';
2217
+ }
2218
+
2219
+ graphicsCapacity(): GraphicsCapacity {
2220
+ return {
2221
+ maxNodes: 256,
2222
+ maxBindings: 64,
2223
+ maxActiveTransitions: 32,
2224
+ nodeStorage: 'flash',
2225
+ };
2226
+ }
2227
+
2228
+ // ── Debug ─────────────────────────────────────────────────────────────────
2229
+ // Source-level debugging is the GDB flow only (the west-owned gdb server
2230
+ // serves F5 via cortex-debug's external-server mode — see
2231
+ // toolchain/debug-config.ts). The printf/Serial breakpoint instrumentation
2232
+ // pipeline was removed from the product.
2233
+
2234
+ debugMode(target?: string): 'gdb' | 'none' {
2235
+ // `target` is the Zephyr board id (optionally with a /qualifier suffix).
2236
+ // gdb capability is a FACT of the board: its probe-method table carries a
2237
+ // debug-capable entry (bootloaders mark debug:false; SWD/JTAG probes and
2238
+ // built-in USB-JTAG carry debug:true) — the same table west's runner
2239
+ // selection uses. Boards without a table (or unresolvable targets, e.g.
2240
+ // generated custom boards) report 'none', and --debug fails with an
2241
+ // explicit diagnostic (transpile.ts) rather than building silently.
2242
+ const t = (target ?? '').trim();
2243
+ if (!t) return 'none';
2244
+ const memo = debugModeMemo.get(t);
2245
+ if (memo) return memo;
2246
+ let mode: 'gdb' | 'none' = 'none';
2247
+ try {
2248
+ const g = generateBoard(t);
2249
+ const constants = JSON.parse(g.boardJson).constants as Record<string, unknown>;
2250
+ for (let i = 0; ; i++) {
2251
+ const id = constants[`zephyr.probeMethods.${i}.id`];
2252
+ if (typeof id !== 'string') break;
2253
+ if (constants[`zephyr.probeMethods.${i}.debug`] !== false) {
2254
+ mode = 'gdb';
2255
+ break;
2256
+ }
2257
+ }
2258
+ } catch {
2259
+ // Board not resolvable from facts — 'none' (never crash codegen over
2260
+ // the debug-capability gate).
2261
+ }
2262
+ debugModeMemo.set(t, mode);
2263
+ return mode;
2264
+ }
2265
+ }