@typecad/framework-arduino 0.1.0-alpha.1

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/dist/arduino/i2c-arduino.d.ts +47 -0
  4. package/dist/arduino/i2c-arduino.js +6 -0
  5. package/dist/arduino/index.d.ts +6 -0
  6. package/dist/arduino/index.js +8 -0
  7. package/dist/arduino/spi-arduino.d.ts +42 -0
  8. package/dist/arduino/spi-arduino.js +6 -0
  9. package/dist/arduino/stubs-uno.d.ts +9 -0
  10. package/dist/arduino/stubs-uno.js +65 -0
  11. package/dist/arduino/uart-arduino.d.ts +41 -0
  12. package/dist/arduino/uart-arduino.js +6 -0
  13. package/dist/arduino-class-map.d.ts +5 -0
  14. package/dist/arduino-class-map.js +21 -0
  15. package/dist/arduino-compile.d.ts +24 -0
  16. package/dist/arduino-compile.js +316 -0
  17. package/dist/arduino-libs.d.ts +6 -0
  18. package/dist/arduino-libs.js +11 -0
  19. package/dist/cli-metadata.d.ts +12 -0
  20. package/dist/cli-metadata.js +196 -0
  21. package/dist/cpp-parser.d.ts +47 -0
  22. package/dist/cpp-parser.js +292 -0
  23. package/dist/debug-codegen.d.ts +20 -0
  24. package/dist/debug-codegen.js +115 -0
  25. package/dist/displays/ili9341-spi.d.ts +6 -0
  26. package/dist/displays/ili9341-spi.js +28 -0
  27. package/dist/displays/ssd1309-i2c.d.ts +2 -0
  28. package/dist/displays/ssd1309-i2c.js +16 -0
  29. package/dist/displays/st7796-spi.d.ts +2 -0
  30. package/dist/displays/st7796-spi.js +20 -0
  31. package/dist/graphics/ili9341.d.ts +24 -0
  32. package/dist/graphics/ili9341.js +59 -0
  33. package/dist/index.d.ts +13 -0
  34. package/dist/index.js +27 -0
  35. package/dist/lib-declaration.d.ts +35 -0
  36. package/dist/lib-declaration.js +399 -0
  37. package/dist/lib-discovery.d.ts +38 -0
  38. package/dist/lib-discovery.js +189 -0
  39. package/dist/profile.d.ts +8 -0
  40. package/dist/profile.js +584 -0
  41. package/dist/strategy.d.ts +174 -0
  42. package/dist/strategy.js +2032 -0
  43. package/package.json +73 -0
  44. package/src/arduino/i2c-arduino.ts +72 -0
  45. package/src/arduino/index.ts +18 -0
  46. package/src/arduino/spi-arduino.ts +63 -0
  47. package/src/arduino/stubs-uno.ts +80 -0
  48. package/src/arduino/uart-arduino.ts +59 -0
  49. package/src/arduino-class-map.ts +32 -0
  50. package/src/arduino-compile.ts +359 -0
  51. package/src/arduino-libs.ts +33 -0
  52. package/src/cli-metadata.ts +250 -0
  53. package/src/cpp-parser.ts +361 -0
  54. package/src/debug-codegen.ts +155 -0
  55. package/src/displays/ili9341-spi.ts +33 -0
  56. package/src/displays/ssd1309-i2c.ts +19 -0
  57. package/src/displays/st7796-spi.ts +23 -0
  58. package/src/graphics/ili9341.ts +80 -0
  59. package/src/index.ts +37 -0
  60. package/src/lib-declaration.ts +493 -0
  61. package/src/lib-discovery.ts +267 -0
  62. package/src/profile.ts +676 -0
  63. package/src/strategy.ts +2126 -0
@@ -0,0 +1,2126 @@
1
+ // ---------------------------------------------------------------------------
2
+ // ArduinoStrategy — Arduino framework target (setup/loop, Serial, .ino …)
3
+ //
4
+ // Absorbs all Arduino-specific emit logic previously scattered across
5
+ // cpp-emitter.ts, TypeCAD-map.ts, and arduino-profile.ts.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import type { PlatformStrategy, ExpressionIR, ProgramIR, Diagnostic, PlatformContext, BoardConstants, RuntimePolyfillIR, StdLibSupport, AsyncRuntimeConfig, GraphicsCapacity, DisplayHALOp } from "@typecad/cuttlefish/api/shared";
9
+ import type { StatementIR, HALOpIR } from "@typecad/cuttlefish/api/shared";
10
+ import { generatePromiseRuntime, generateStaticAsyncRuntime, applyStringMethodRewrites, parsedIsVector } from "@typecad/cuttlefish/api/shared";
11
+ import { generateSerialInitCode, generateBreakpointCode, generateLogpointCode } from "./debug-codegen.js";
12
+ import { resolveArduinoProfile } from "./profile.js";
13
+ import { resolveILI9341Op, ILI9341Context } from "./graphics/ili9341.js";
14
+
15
+ /**
16
+ * Arduino-specific platform context.
17
+ * Accessed via `PlatformContext` index signature: `arduinoCtx(ctx)`.
18
+ */
19
+ export interface ArduinoPlatformContext {
20
+ buildTarget?: string;
21
+ }
22
+
23
+ function arduinoCtx(ctx?: PlatformContext): ArduinoPlatformContext | undefined {
24
+ const data = ctx?.frameworkData as { buildTarget?: string } | undefined;
25
+ return data ?? undefined;
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Constant sets – previously module-level in cpp-emitter.ts
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Names predefined by the Arduino / ESP32 framework as macros or globals.
34
+ * Emitting C++ declarations with these names causes redeclaration errors.
35
+ */
36
+ const ARDUINO_RESERVED_NAMES: ReadonlySet<string> = new Set([
37
+ // Standard Arduino digital/analog pin-mode macros (all platforms)
38
+ // HIGH and LOW are phantom constants from @typecad/board that map directly to Arduino
39
+ // macros — they must pass through as-is in expressions.
40
+ "INPUT", "OUTPUT", "INPUT_PULLUP", "RISING", "FALLING", "CHANGE",
41
+ // ESP32-specific pin-mode macros (esp32-hal-gpio.h)
42
+ "INPUT_PULLDOWN", "OUTPUT_OPEN_DRAIN", "ANALOG",
43
+ // Bus pin aliases (all platforms)
44
+ "SDA", "SCL", "SS", "MOSI", "MISO", "SCK",
45
+ // UART pin aliases — predefined as static const uint8_t on most platforms
46
+ "TX", "RX", "TX2", "RX2",
47
+ // DAC channel aliases — predefined as static const uint8_t on ESP32
48
+ "DAC1", "DAC2",
49
+ // ADC channel aliases — predefined on all Arduino platforms
50
+ "A0", "A1", "A2", "A3", "A4", "A5",
51
+ // Predefined HardwareSerial globals
52
+ "Serial", "Serial2",
53
+ // Arduino.h analog reference macros
54
+ "DEFAULT", "INTERNAL", "EXTERNAL",
55
+ // C++ math macros exposed by Arduino headers
56
+ "min", "max",
57
+ // CMSIS / device-header macros (SAMD21 defines RTC as a register pointer)
58
+ "RTC",
59
+ ]);
60
+
61
+ /**
62
+ * Enum member names that conflict with Arduino / ESP32 framework macros.
63
+ * Prefixed with `_` in the emitted enum class body.
64
+ */
65
+ const ARDUINO_ENUM_MEMBER_RENAMES: ReadonlySet<string> = new Set([
66
+ "HIGH", "LOW", "INPUT", "OUTPUT", "INPUT_PULLUP",
67
+ "RISING", "FALLING", "CHANGE",
68
+ "INPUT_PULLDOWN", "OUTPUT_OPEN_DRAIN", "ANALOG",
69
+ "DEFAULT", "INTERNAL", "EXTERNAL",
70
+ "RTC",
71
+ ]);
72
+
73
+ /**
74
+ * Arduino framework identifiers that are value/function-like *macros* and so
75
+ * must be emitted verbatim when referenced in an expression (e.g.
76
+ * `pinMode(13, OUTPUT)`). Unlike the redeclaration hazards in
77
+ * ARDUINO_RESERVED_NAMES, these are meant to be *invoked* — escaping them to
78
+ * `OUTPUT_` would emit an undefined symbol. HIGH and LOW are included because
79
+ * they too are Arduino macros that must pass through (they are phantom
80
+ * constants from @typecad/board that map directly to the Arduino HIGH/LOW macros).
81
+ */
82
+ const ARDUINO_PASSTHROUGH_MACROS: ReadonlySet<string> = new Set([
83
+ "INPUT", "OUTPUT", "INPUT_PULLUP", "INPUT_PULLDOWN",
84
+ "OUTPUT_OPEN_DRAIN", "ANALOG",
85
+ "HIGH", "LOW",
86
+ "RISING", "FALLING", "CHANGE",
87
+ "DEFAULT", "INTERNAL", "EXTERNAL",
88
+ ]);
89
+
90
+ /**
91
+ * Enum class names already declared as C typedefs in the new Arduino API.
92
+ */
93
+ const ARDUINO_API_RESERVED_ENUMS: ReadonlySet<string> = new Set([
94
+ "PinMode", "InterruptMode",
95
+ ]);
96
+
97
+ // Shared set of large enum names (values > 16-bit signed int range)
98
+ // populated externally via setLargeEnumNames().
99
+ let _largeEnumNames: ReadonlySet<string> = new Set();
100
+
101
+ /**
102
+ * Detect whether a program references the cooperative async runtime.
103
+ *
104
+ * The HAL `Async` singleton methods (sleep/yield/sleepUntil/currentTask) lower
105
+ * to calls on `__cuttlefish_async_*` symbols defined by the promise runtime.
106
+ * Unlike `async function` declarations, these calls are not flagged on the
107
+ * FunctionIR (fn.isAsync), so the promise runtime would not be linked and the
108
+ * symbols would be undefined. This walks the program's statements/expressions
109
+ * looking for any `raw` IR node whose text references an async-runtime symbol.
110
+ */
111
+ function programUsesAsyncRuntime(program: ProgramIR): boolean {
112
+ const ASYNC_TOKEN = "__cuttlefish_async_";
113
+ // Sentinel thrown to short-circuit the walk once a reference is found.
114
+ const FOUND = {};
115
+ const seen = new Set<object>();
116
+ const walkExpr = (expr: ExpressionIR): void => {
117
+ if (!expr || typeof expr !== "object") return;
118
+ if (seen.has(expr)) return;
119
+ seen.add(expr);
120
+ const e = expr as Record<string, any>;
121
+ if (e.kind === "raw" && typeof e.value === "string" && e.value.includes(ASYNC_TOKEN)) {
122
+ throw FOUND;
123
+ }
124
+ // HAL method calls that lower via rawCpp() become `hal-expr` nodes whose
125
+ // `operation` is { operation: "raw", code: "..." }. The Async singleton's
126
+ // methods (sleep/yield/sleepUntil/currentTask) land here, so inspect the
127
+ // resolved code text for the async-runtime symbol token.
128
+ if (e.kind === "hal-expr" && e.operation && typeof e.operation === "object") {
129
+ const op = e.operation as Record<string, any>;
130
+ if (op.operation === "raw" && typeof op.code === "string" && op.code.includes(ASYNC_TOKEN)) {
131
+ throw FOUND;
132
+ }
133
+ }
134
+ for (const v of Object.values(e)) {
135
+ if (Array.isArray(v)) {
136
+ for (const item of v) {
137
+ if (item && typeof item === "object") {
138
+ if ("kind" in item && typeof item.kind === "string") {
139
+ walkExpr(item as ExpressionIR);
140
+ } else {
141
+ walkStmt(item as StatementIR);
142
+ }
143
+ }
144
+ }
145
+ } else if (v && typeof v === "object" && "kind" in v && typeof v.kind === "string") {
146
+ walkExpr(v as ExpressionIR);
147
+ }
148
+ }
149
+ };
150
+ const walkStmt = (stmt: StatementIR): void => {
151
+ if (!stmt || typeof stmt !== "object") return;
152
+ if (seen.has(stmt)) return;
153
+ seen.add(stmt);
154
+ const s = stmt as Record<string, any>;
155
+ // HAL method calls that lower via rawCpp() become `hal-op` statements whose
156
+ // `operation` is { operation: "raw", code: "..." }. The Async singleton's
157
+ // methods (sleep/yield/sleepUntil/currentTask) land here as statement-level
158
+ // ops, so inspect the resolved code text for the async-runtime token.
159
+ if (s.kind === "hal-op" && s.operation && typeof s.operation === "object") {
160
+ const op = s.operation as Record<string, any>;
161
+ if (op.operation === "raw" && typeof op.code === "string" && op.code.includes(ASYNC_TOKEN)) {
162
+ throw FOUND;
163
+ }
164
+ }
165
+ for (const v of Object.values(s)) {
166
+ if (Array.isArray(v)) {
167
+ for (const item of v) {
168
+ if (item && typeof item === "object") {
169
+ if ("body" in item || "statements" in item || "thenBranch" in item || "cases" in item) {
170
+ walkStmt(item as StatementIR);
171
+ } else if ("kind" in item && typeof item.kind === "string") {
172
+ walkExpr(item as ExpressionIR);
173
+ }
174
+ }
175
+ }
176
+ } else if (v && typeof v === "object" && "kind" in v && typeof v.kind === "string") {
177
+ walkExpr(v as ExpressionIR);
178
+ } else if (v && typeof v === "object") {
179
+ walkStmt(v as StatementIR);
180
+ }
181
+ }
182
+ };
183
+ try {
184
+ for (const fn of program.functions) {
185
+ for (const stmt of fn.statements) walkStmt(stmt);
186
+ }
187
+ } catch (e) {
188
+ if (e === FOUND) return true;
189
+ throw e;
190
+ }
191
+ return false;
192
+ }
193
+
194
+ export class ArduinoStrategy implements PlatformStrategy {
195
+ readonly id = "arduino";
196
+
197
+ // Cached profile to avoid repeated arduino-cli calls
198
+ private _cachedProfile: ReturnType<typeof resolveArduinoProfile> | null = null;
199
+ private _cachedProfileKey: string | null = null;
200
+ /** Architecture extracted from FQBN, used for ISR attribute emission. */
201
+ private _cachedArch: string = 'default';
202
+ /** Track whether the current program uses createPinGroup() */
203
+ private _usesPinGroup: boolean = false;
204
+ /**
205
+ * Allows the emitter to inform this strategy which enums have large values
206
+ * so that static_cast uses `long` instead of `int`.
207
+ */
208
+ setLargeEnumNames(names: ReadonlySet<string>): void {
209
+ _largeEnumNames = names;
210
+ }
211
+
212
+ /**
213
+ * Clears the cached profile. Should be called between transpilations.
214
+ */
215
+ clearProfileCache(): void {
216
+ this._cachedProfile = null;
217
+ this._cachedProfileKey = null;
218
+ this._cachedArch = 'default';
219
+ }
220
+
221
+ /**
222
+ * Gets or resolves the Arduino profile, caching the result.
223
+ */
224
+ private getOrResolveProfile(program: ProgramIR, ctx?: PlatformContext): ReturnType<typeof resolveArduinoProfile> {
225
+ const key = arduinoCtx(ctx)?.buildTarget ?? 'default';
226
+
227
+ if (this._cachedProfile && this._cachedProfileKey === key) {
228
+ return this._cachedProfile;
229
+ }
230
+
231
+ this._cachedProfile = resolveArduinoProfile(program, ctx);
232
+ this._cachedProfileKey = key;
233
+ // Cache architecture for use by isrFunctionAttribute()
234
+ const buildTarget = arduinoCtx(ctx)?.buildTarget;
235
+ if (buildTarget) {
236
+ const parts = buildTarget.split(':');
237
+ this._cachedArch = parts.length >= 2 ? parts[1] : 'default';
238
+ } else {
239
+ this._cachedArch = 'default';
240
+ }
241
+ return this._cachedProfile;
242
+ }
243
+
244
+ // ── Profile ─────────────────────────────────────────────────────────────
245
+
246
+ forcedIncludes(program: ProgramIR, ctx?: PlatformContext): string[] {
247
+ return this.getOrResolveProfile(program, ctx).forcedIncludes;
248
+ }
249
+ symbolAliases(program: ProgramIR, ctx?: PlatformContext): Record<string, string> {
250
+ return this.getOrResolveProfile(program, ctx).symbolAliases;
251
+ }
252
+ shimLines(program: ProgramIR, ctx?: PlatformContext): string[] {
253
+ this._usesPinGroup = detectPinGroupUsage(program);
254
+ const profileLines = this.getOrResolveProfile(program, ctx).shimLines;
255
+ const lines: string[] = [];
256
+
257
+ // Only emit nullish/undefined shims if the program actually uses them.
258
+ // The transpiler replaces `undefined`/`null` with CUTTLEFISH_UNDEFINED and
259
+ // `??` with cuttlefish_nullish — if neither appears, the shims are dead code.
260
+ const usesNullish = programAnalysisUsesNullish(program);
261
+ if (usesNullish) {
262
+ lines.push(
263
+ "// TypeCAD Core Shims",
264
+ "#ifndef CUTTLEFISH_UNDEFINED",
265
+ "#define CUTTLEFISH_UNDEFINED 0",
266
+ "#endif",
267
+ "",
268
+ "// Nullish helpers — overload set so value/struct types (which always",
269
+ "// exist) return false from the generic template, while scalars compare",
270
+ "// against CUTTLEFISH_UNDEFINED. The generic catch-all must NOT cast",
271
+ "// (T)CUTTLEFISH_UNDEFINED — that fails to compile for non-scalar T.",
272
+ "template<typename T> inline bool cuttlefish_is_nullish(const T&) { return false; }",
273
+ "inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }",
274
+ "inline bool cuttlefish_is_nullish(long v) { return v == CUTTLEFISH_UNDEFINED; }",
275
+ "inline bool cuttlefish_is_nullish(double v) { return v == (double)CUTTLEFISH_UNDEFINED; }",
276
+ "inline bool cuttlefish_is_nullish(bool v) { return v == false; }",
277
+ "template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }",
278
+ "template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }",
279
+ "template<typename T, typename U> inline T cuttlefish_nullish(const T& a, const U& b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }",
280
+ "",
281
+ );
282
+ }
283
+
284
+ if (this._usesPinGroup) {
285
+ lines.push(
286
+ "// PinGroup polyfill",
287
+ "struct __tc_PinGroup {",
288
+ " const int* pins;",
289
+ " int count;",
290
+ " void writePattern(int pattern) const {",
291
+ " for (int i = 0; i < count; i++) {",
292
+ " digitalWrite(pins[i], (pattern >> i) & 1 ? HIGH : LOW);",
293
+ " }",
294
+ " }",
295
+ " int readPattern() const {",
296
+ " int value = 0;",
297
+ " for (int i = 0; i < count; i++) {",
298
+ " if (digitalRead(pins[i]) == HIGH) {",
299
+ " value |= (1 << i);",
300
+ " }",
301
+ " }",
302
+ " return value;",
303
+ " }",
304
+ " void fill(bool value) const {",
305
+ " for (int i = 0; i < count; i++) {",
306
+ " digitalWrite(pins[i], value ? HIGH : LOW);",
307
+ " }",
308
+ " }",
309
+ "};",
310
+ "template<typename... Args>",
311
+ "__tc_PinGroup __tc_createPinGroup(Args... args) {",
312
+ " static const int pins[] = { args... };",
313
+ " for(int i=0; i<sizeof...(args); i++) pinMode(pins[i], OUTPUT);",
314
+ " return __tc_PinGroup{pins, sizeof...(args)};",
315
+ "}"
316
+ );
317
+ }
318
+
319
+ // Comprehensive HAL polyfills in C++
320
+ lines.push(
321
+ "// TypeCAD Native Polyfills",
322
+ "struct __tc_Num {",
323
+ " struct MapChain {",
324
+ " long v; long fl, fh;",
325
+ " MapChain(long _v) : v(_v), fl(0), fh(1023) {}",
326
+ " MapChain& from(long l, long h) { fl = l; fh = h; return *this; }",
327
+ " long to(long l, long h) { return (v - fl) * (h - l) / (fh - fl) + l; }",
328
+ " long toPercent() { return (v - fl) * 100 / (fh - fl); }",
329
+ " long toByte() { return (v - fl) * 255 / (fh - fl); }",
330
+ " };",
331
+ " struct ConstrainChain {",
332
+ " long v;",
333
+ " ConstrainChain(long _v) : v(_v) {}",
334
+ " long between(long l, long h) { return v < l ? l : (v > h ? h : v); }",
335
+ " };",
336
+ " static long (_abs)(long x) { return x < 0 ? -x : x; }",
337
+ " static long (_min)(long a, long b) { return a < b ? a : b; }",
338
+ " static long (_max)(long a, long b) { return a > b ? a : b; }",
339
+ " static MapChain (_map)(long v) { return MapChain(v); }",
340
+ " static ConstrainChain (_constrain)(long v) { return ConstrainChain(v); }",
341
+ "} Num;",
342
+ "",
343
+ "struct __tc_Timing {",
344
+ " unsigned long millis() { return ::millis(); }",
345
+ " unsigned long micros() { return ::micros(); }",
346
+ " void delay(unsigned long ms) { ::delay(ms); }",
347
+ " void delayMicroseconds(unsigned int us) { ::delayMicroseconds(us); }",
348
+ );
349
+
350
+ // freeHeap() — architecture-specific, resolved at transpile time
351
+ const arch = this._cachedArch;
352
+ if (arch === 'esp32' || arch === 'esp32s2' || arch === 'esp32s3' || arch === 'esp32c3' || arch === 'esp32c6') {
353
+ lines.push(
354
+ " unsigned long freeHeap() {",
355
+ " return ESP.getFreeHeap();",
356
+ " }",
357
+ );
358
+ } else if (arch === 'avr' || arch === 'megaavr') {
359
+ lines.push(
360
+ " unsigned long freeHeap() {",
361
+ " extern int __heap_start, *__brkval;",
362
+ " int v;",
363
+ " return (unsigned long) &v - (__brkval == 0 ? (unsigned long) &__heap_start : (unsigned long) __brkval);",
364
+ " }",
365
+ );
366
+ } else {
367
+ lines.push(
368
+ " unsigned long freeHeap() {",
369
+ " return 0;",
370
+ " }",
371
+ );
372
+ }
373
+
374
+ lines.push(
375
+ "} Timing;",
376
+ "",
377
+ );
378
+
379
+ // WDT — only emitted on AVR architectures that support it
380
+ if (arch === 'avr' || arch === 'megaavr') {
381
+ lines.push(
382
+ "#include <avr/wdt.h>",
383
+ "#include <string.h>",
384
+ "struct __tc_WDT {",
385
+ " void (enable)(const char* t) {",
386
+ " if (strcmp(t, \"15ms\") == 0) wdt_enable(WDTO_15MS);",
387
+ " else if (strcmp(t, \"30ms\") == 0) wdt_enable(WDTO_30MS);",
388
+ " else if (strcmp(t, \"60ms\") == 0) wdt_enable(WDTO_60MS);",
389
+ " else if (strcmp(t, \"120ms\") == 0) wdt_enable(WDTO_120MS);",
390
+ " else if (strcmp(t, \"250ms\") == 0) wdt_enable(WDTO_250MS);",
391
+ " else if (strcmp(t, \"500ms\") == 0) wdt_enable(WDTO_500MS);",
392
+ " else if (strcmp(t, \"1s\") == 0) wdt_enable(WDTO_1S);",
393
+ " else if (strcmp(t, \"2s\") == 0) wdt_enable(WDTO_2S);",
394
+ " else if (strcmp(t, \"4s\") == 0) wdt_enable(WDTO_4S);",
395
+ " else if (strcmp(t, \"8s\") == 0) wdt_enable(WDTO_8S);",
396
+ " }",
397
+ " void (enable)(int t) { wdt_enable(t); }",
398
+ " void (reset)() { wdt_reset(); }",
399
+ " void (disable)() { wdt_disable(); }",
400
+ "} WDT;",
401
+ );
402
+ }
403
+
404
+ lines.push(
405
+ "",
406
+ "#ifndef CUTTLEFISH_STR_BUF_SIZE",
407
+ "#define CUTTLEFISH_STR_BUF_SIZE 64",
408
+ "#endif",
409
+ "",
410
+ "// String helpers",
411
+ "struct __tc_str_ptr {",
412
+ " char buf[CUTTLEFISH_STR_BUF_SIZE];",
413
+ " __tc_str_ptr(const char* s = \"\") { strncpy(buf, s, CUTTLEFISH_STR_BUF_SIZE - 1); buf[CUTTLEFISH_STR_BUF_SIZE - 1] = 0; }",
414
+ " // Copy only up to and NUL: every setter writes a terminator within",
415
+ " // bounds and all readers stop at NUL, so the tail is never observed.",
416
+ " // Bounds the work to content length instead of the full buffer.",
417
+ " __tc_str_ptr(const __tc_str_ptr& o) { memcpy(buf, o.buf, ::strlen(o.buf) + 1); }",
418
+ " __tc_str_ptr& operator=(const __tc_str_ptr& o) { memcpy(buf, o.buf, ::strlen(o.buf) + 1); return *this; }",
419
+ " __tc_str_ptr& operator=(const char* s) { strncpy(buf, s, CUTTLEFISH_STR_BUF_SIZE - 1); buf[CUTTLEFISH_STR_BUF_SIZE - 1] = 0; return *this; }",
420
+ " const char* c_str() const { return buf; }",
421
+ " size_t size() const { return ::strlen(buf); }",
422
+ " size_t length() const { return ::strlen(buf); }",
423
+ " int indexOf(const char* s) const { const char* p = strstr(buf, s); return p ? p - buf : -1; }",
424
+ " operator const char*() const { return buf; }",
425
+ " bool operator==(const char* o) const { return strcmp(buf, o) == 0; }",
426
+ " bool operator!=(const char* o) const { return strcmp(buf, o) != 0; }",
427
+ " bool operator==(const __tc_str_ptr& o) const { return strcmp(buf, o.buf) == 0; }",
428
+ " bool operator!=(const __tc_str_ptr& o) const { return strcmp(buf, o.buf) != 0; }",
429
+ "};",
430
+ "inline size_t (strlen)(const __tc_str_ptr& s) { return ::strlen(s.buf); }"
431
+ );
432
+
433
+ lines.push(...profileLines);
434
+ return lines;
435
+ }
436
+ profileDiagnostics(program: ProgramIR, ctx?: PlatformContext): Diagnostic[] {
437
+ // Copy the cached profile's diagnostics into a FRESH array — the profile
438
+ // is cached by buildTarget (getOrResolveProfile), so mutating its
439
+ // `.diagnostics` array (by pushing the architecture-specific gates below)
440
+ // would accumulate diagnostics across transpilations on a shared strategy
441
+ // instance (the test harness reuses one ArduinoStrategy). Snapshot first.
442
+ const base = [...this.getOrResolveProfile(program, ctx).diagnostics];
443
+ // AVR-class targets (ATmega, megaAVR) ship NO <vector>/<string>/<iostream>
444
+ // and discourage heap allocation (see STDLIB_SUPPORT: hasVector=false,
445
+ // recommendedArrayImpl="static_array"). A function-LOCAL array initialized
446
+ // from a literal lowers to a fixed-size __tc_StaticArray<T,N> (the literal
447
+ // supplies N) and is fully supported — but a CLASS FIELD, a FUNCTION
448
+ // PARAMETER, or a RETURN TYPE annotated `T[]` / `Array<T>` resolves to
449
+ // `std::vector<T>`, which has NO valid lowering on these targets:
450
+ // • there is no literal at the declaration site to recover a compile-time
451
+ // size for __tc_StaticArray<T,N>, AND
452
+ // • the storage is dynamically grown (.push in a loop) in the idiomatic
453
+ // case, which a fixed-size buffer cannot model.
454
+ // Rather than emit `std::vector<T>` and let avr-g++ fail with an opaque
455
+ // "'vector' in namespace 'std' does not name a template type", surface a
456
+ // single clear, source-located error per offending site. This catches the
457
+ // whole family (fields, params, returns) at transpile time. The general
458
+ // `isHeapAllocationUnsafe` / `isExceptionSupportDisabled` AVR guards below
459
+ // follow the same architecture-gating pattern.
460
+ const arch = (ctx?.architecture ?? arduinoCtx(ctx)?.buildTarget?.split(":")?.[1] ?? "").toLowerCase();
461
+ const stdlib = this.getStdLibSupport(arch);
462
+ if (!stdlib.hasVector) {
463
+ base.push(...collectNoVectorStorageDiagnostics(program));
464
+ }
465
+ // Heap allocation via `new` is unsafe on no-heap architectures (AVR has
466
+ // 2 KB SRAM and no heap manager). This is a TARGET-AWARE gate, so it lives
467
+ // here in profileDiagnostics (which sees the FQBN-derived `arch`) rather
468
+ // than in the build-time IR validator — that validator only sees
469
+ // `boardConstants.architecture`, which is populated by the MCU/board
470
+ // import and is therefore ABSENT for programs with no such import (so the
471
+ // build-time gate silently missed `new` sites in import-less programs).
472
+ // Surfacing it here makes detection independent of import structure
473
+ // (demo #34 Finding A). Detection keys off the `newClassName` marker a
474
+ // user-class `new` now tags its raw IR node with, falling back to the
475
+ // text regex for untagged/hand-built IR.
476
+ // Heap-allocation detector always runs; severity scales with the target
477
+ // (warning+error on AVR, info elsewhere). See collectHeapAllocationDiagnostics.
478
+ base.push(...collectHeapAllocationDiagnostics(program, arch, this.isHeapAllocationUnsafe(arch)));
479
+ return base;
480
+ }
481
+
482
+ // ── Polyfill overrides ──────────────────────────────────────────────────
483
+
484
+ /**
485
+ * Default implementation returns empty set.
486
+ * Board packages can override to provide native implementations.
487
+ */
488
+ nativePolyfills(): Set<string> {
489
+ return new Set(["string_methods", "cuttlefish_halt", "timer_methods"]);
490
+ }
491
+
492
+ /**
493
+ * Generate native helpers: cuttlefish_halt macro, Arduino string helpers,
494
+ * and (when stdlib supports it) the cooperative async Promise runtime.
495
+ */
496
+ generateNativePolyfills(program: ProgramIR, ctx?: PlatformContext): RuntimePolyfillIR[] {
497
+ const helpers: RuntimePolyfillIR[] = [{
498
+ kind: "polyfill",
499
+ id: "cuttlefish_halt",
500
+ domain: "arduino",
501
+ requiredIncludes: [],
502
+ forwardDeclarations: [],
503
+ helperStructs: [],
504
+ helperFunctions: [
505
+ `#ifndef cuttlefish_halt
506
+ #define cuttlefish_halt(msg) do { Serial.println(F(msg)); for (;;) {} } while (0)
507
+ #endif`,
508
+ ],
509
+ shimMacros: [],
510
+ dependencies: [],
511
+ }, {
512
+ kind: "polyfill",
513
+ id: "string_methods",
514
+ domain: "arduino",
515
+ requiredIncludes: [],
516
+ forwardDeclarations: [],
517
+ helperStructs: [],
518
+ helperFunctions: [`
519
+ // Arduino string method polyfills
520
+ // CUTTLEFISH_STR_BUF_SIZE guard is idempotent — shimLines re-declares it,
521
+ // but polyfill helperFunctions emit BEFORE shimLines, so we must define it
522
+ // here too for the static buffers below to compile.
523
+ #ifndef CUTTLEFISH_STR_BUF_SIZE
524
+ #define CUTTLEFISH_STR_BUF_SIZE 64
525
+ #endif
526
+ 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; }
527
+ 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++) *p = toupper(*p); return b; }
528
+ 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++) *p = tolower(*p); return b; }
529
+ 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; }
530
+ 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; }
531
+ const char* __tc_substring1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
532
+ const char* __tc_slice2(const char* s, int start, int end) { return __tc_substring2(s, start, end); }
533
+ const char* __tc_slice1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
534
+ 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 = (int)(pos - s); int oldLen = (int)strlen(old); int replLen = (int)strlen(repl); if (beforeLen + replLen + (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; }
535
+ 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]; }
536
+ int __tc_charCodeAt(const char* s, int idx) { return (int)(unsigned char)s[idx]; }
537
+ int __tc_indexOf(const char* s, const char* needle) { const char* p = strstr(s, needle); return p ? (int)(p - s) : -1; }
538
+ `],
539
+ shimMacros: [],
540
+ dependencies: [],
541
+ }, {
542
+ kind: "polyfill",
543
+ id: "static_array",
544
+ domain: "arduino",
545
+ requiredIncludes: [],
546
+ forwardDeclarations: [],
547
+ helperStructs: [],
548
+ // __tc_StaticArray is also defined in transpiler-support.h (when that
549
+ // header is emitted). To be safe in BOTH cases (header present or not),
550
+ // wrap the definition in an idempotent guard so a redefinition is a
551
+ // no-op rather than an error. Emitted via helperFunctions with a marker
552
+ // fn so filterPolyfillHelpers keeps it when __tc_StaticArray appears in
553
+ // user code. Demo #23 Finding A (revised).
554
+ helperFunctions: [`
555
+ #ifndef __TC_STATIC_ARRAY_DEFINED
556
+ #define __TC_STATIC_ARRAY_DEFINED
557
+ template<typename T, int N>
558
+ struct __tc_StaticArray {
559
+ T data[N];
560
+ int _size;
561
+ __tc_StaticArray() : _size(0) {}
562
+ int length() const { return _size; }
563
+ int size() const { return _size; }
564
+ void push(T val) { if (_size < N) data[_size++] = val; }
565
+ T pop() { return (_size > 0) ? data[--_size] : T(); }
566
+ int indexOf(T val) const { for (int i = 0; i < _size; i++) if (data[i] == val) return i; return -1; }
567
+ T& operator[](int i) { return data[i]; }
568
+ const T& operator[](int i) const { return data[i]; }
569
+ T* begin() { return &data[0]; }
570
+ T* end() { return &data[_size]; }
571
+ const T* begin() const { return &data[0]; }
572
+ const T* end() const { return &data[_size]; }
573
+ };
574
+ #endif
575
+ `],
576
+ shimMacros: [],
577
+ dependencies: [],
578
+ }];
579
+
580
+ // Add cooperative timer methods if used. Callbacks run from loop(), so
581
+ // generated UI/state mutations stay on the main Arduino execution path.
582
+ // Size MAX_TIMERS to the observed setInterval/setTimeout call count rather
583
+ // than a blind constant: a one-timer program links one slot, not eight.
584
+ // The filterPolyfillHelpers gate already strips this polyfill entirely when
585
+ // no timer calls exist; this sizes it correctly when they do. Floor of 1
586
+ // keeps the array well-formed even if analysis is conservative.
587
+ const analysis = (ctx as { analysis?: { timerCallCount?: number } } | undefined)?.analysis;
588
+ const observedTimers = analysis?.timerCallCount ?? 0;
589
+ const maxTimers = Math.max(1, observedTimers);
590
+ helpers.push({
591
+ kind: "polyfill",
592
+ id: "timer_methods",
593
+ domain: "arduino",
594
+ requiredIncludes: [],
595
+ forwardDeclarations: [],
596
+ helperStructs: [`
597
+ struct __tc_TimerTask {
598
+ void (*callback)();
599
+ unsigned long interval;
600
+ unsigned long lastRun;
601
+ bool repeat;
602
+ bool active;
603
+ };
604
+
605
+ class __tc_TimerRuntime {
606
+ static const int MAX_TIMERS = ${maxTimers};
607
+ __tc_TimerTask tasks[MAX_TIMERS];
608
+ public:
609
+ __tc_TimerRuntime() {
610
+ for (int i=0; i<MAX_TIMERS; i++) tasks[i].active = false;
611
+ }
612
+ int add(void (*cb)(), unsigned long ms, bool repeat) {
613
+ for (int i=0; i<MAX_TIMERS; i++) {
614
+ if (!tasks[i].active) {
615
+ tasks[i].callback = cb;
616
+ tasks[i].interval = ms;
617
+ tasks[i].lastRun = millis();
618
+ tasks[i].repeat = repeat;
619
+ tasks[i].active = true;
620
+ return i + 1;
621
+ }
622
+ }
623
+ return 0;
624
+ }
625
+ void clear(int id) {
626
+ if (id > 0 && id <= MAX_TIMERS) tasks[id-1].active = false;
627
+ }
628
+ void run() {
629
+ unsigned long now = millis();
630
+ for (int i=0; i<MAX_TIMERS; i++) {
631
+ if (tasks[i].active && (now - tasks[i].lastRun >= tasks[i].interval)) {
632
+ tasks[i].callback();
633
+ if (tasks[i].repeat) {
634
+ tasks[i].lastRun = now;
635
+ } else {
636
+ tasks[i].active = false;
637
+ }
638
+ }
639
+ }
640
+ }
641
+ } __tc_timer_runtime;
642
+ `],
643
+ helperFunctions: [`
644
+ int __tc_setInterval(void (*cb)(), long ms) { return __tc_timer_runtime.add(cb, ms, true); }
645
+ int __tc_setTimeout(void (*cb)(), long ms) { return __tc_timer_runtime.add(cb, ms, false); }
646
+ void __tc_clearInterval(int id) { __tc_timer_runtime.clear(id); }
647
+ void __tc_clearTimeout(int id) { __tc_timer_runtime.clear(id); }
648
+ `
649
+ ],
650
+ shimMacros: [],
651
+ dependencies: [],
652
+ });
653
+
654
+ // Add an async runtime if the program declares async functions OR
655
+ // references the async HAL runtime (the `Async` singleton lowers to
656
+ // __cuttlefish_async_* symbols the runtime defines).
657
+ //
658
+ // Two implementations share the same emitted symbol contract:
659
+ // - hasVector && hasString (ESP32/ESP8266/rp2040/samd): the full
660
+ // heap-based Promise<T> runtime (promise-runtime.ts), capacity scaled
661
+ // to the target's RAM.
662
+ // - otherwise (AVR/megaavr, no <vector>/<string>): a heap-free static
663
+ // timer/task-slot runtime (async-runtime-static.ts) with a small
664
+ // fixed capacity sized to the target's SRAM budget.
665
+ // Both define cuttlefish_pump_microtasks() so the loop() injection is
666
+ // identical across targets.
667
+ const hasAsync = program.functions.some(fn => fn.isAsync) || programUsesAsyncRuntime(program);
668
+ if (hasAsync) {
669
+ const architecture = ctx?.architecture ?? arduinoCtx(ctx)?.buildTarget?.split(":")?.[1]?.toLowerCase();
670
+ const stdlib = this.getStdLibSupport(architecture);
671
+ if (stdlib.hasVector && stdlib.hasString) {
672
+ // Heap-based Promise runtime. Capacity was previously selected by
673
+ // buildTarget.split(":")[0] === "arduino", but that prefix is "arduino"
674
+ // for every arduino-cli FQBN (avr AND esp32), making the 256 branch
675
+ // dead. Select by architecture instead.
676
+ const queueCapacity = architecture === "avr" || architecture === "megaavr" ? 32 : 256;
677
+ helpers.push({
678
+ kind: "polyfill",
679
+ id: "async_runtime",
680
+ domain: "arduino",
681
+ requiredIncludes: ["<functional>", "<vector>", "<utility>", "<string>"],
682
+ forwardDeclarations: [],
683
+ helperStructs: [generatePromiseRuntime(queueCapacity, true)],
684
+ helperFunctions: [],
685
+ shimMacros: [],
686
+ dependencies: [],
687
+ hasPromiseRuntime: true,
688
+ } as RuntimePolyfillIR & { hasPromiseRuntime: boolean });
689
+ } else {
690
+ // Heap-free static runtime for no-<vector> targets (AVR/megaavr).
691
+ // Small fixed capacity: AVR has 2 KB SRAM, so 8 slots keeps the timer
692
+ // and task tables well under budget while covering realistic HAL
693
+ // Async.sleep/yield/sleepUntil usage.
694
+ helpers.push({
695
+ kind: "polyfill",
696
+ id: "async_runtime",
697
+ domain: "arduino",
698
+ requiredIncludes: [],
699
+ forwardDeclarations: [],
700
+ helperStructs: [generateStaticAsyncRuntime(8)],
701
+ helperFunctions: [],
702
+ shimMacros: [],
703
+ dependencies: [],
704
+ hasPromiseRuntime: true,
705
+ } as RuntimePolyfillIR & { hasPromiseRuntime: boolean });
706
+ }
707
+ }
708
+
709
+ return helpers;
710
+ }
711
+
712
+ /**
713
+ * Injects Serial.begin(baudRate) at the top of setup() when the program uses
714
+ * console.* calls and has a baudRate configured, and no explicit .begin() call
715
+ * is already present.
716
+ */
717
+ setupInitCode(program: ProgramIR, ctx?: PlatformContext): string[] {
718
+ const baudRate = ctx?.console?.baudRate;
719
+ if (!baudRate) return [];
720
+ const analysis = (ctx as any)?.analysis as { hasConsoleCalls?: boolean; hasSerialBegin?: boolean } | undefined;
721
+ if (analysis) {
722
+ if (!analysis.hasConsoleCalls) return [];
723
+ if (analysis.hasSerialBegin) return [];
724
+ } else {
725
+ // Fallback: walk IR if no pre-computed analysis available
726
+ if (!detectConsoleUsage(program)) return [];
727
+ if (!detectSerialBeginCall(program)) return [];
728
+ }
729
+ return [`Serial.begin(${baudRate});`];
730
+ }
731
+
732
+ // ── File shape ──────────────────────────────────────────────────────────
733
+
734
+ sourceExtension(isEntryFile: boolean, isNpmPackage: boolean): string {
735
+ if (isNpmPackage) return "cpp";
736
+ if (isEntryFile) return "ino";
737
+ return "h";
738
+ }
739
+ entrypointFunctionName(): string {
740
+ return "setup";
741
+ }
742
+ requiresLoopFunction(): boolean {
743
+ return true;
744
+ }
745
+ overrideBaseName(originalBaseName: string, outDirBaseName: string, isEntryFile: boolean, isNpmPackage: boolean): string {
746
+ // Arduino .ino files must match the containing directory name
747
+ return (!isNpmPackage && isEntryFile) ? outDirBaseName : originalBaseName;
748
+ }
749
+ effectiveEmitMode(_requestedMode: string, isNpmPackage: boolean): string {
750
+ return isNpmPackage ? _requestedMode : "cpp";
751
+ }
752
+
753
+ // ── Type normalisation ──────────────────────────────────────────────────
754
+
755
+ defaultNumericType(): string { return "int"; }
756
+ normalizeCppType(typeName: string): string {
757
+ if (typeName === "auto") return "auto";
758
+ if (typeName === "std::string") return "__tc_str_ptr";
759
+ if (typeName === "IInputModePin" || typeName === "IOutputModePin" || typeName === "IPin") return "int";
760
+ if (this._usesPinGroup && typeName.startsWith("IPinGroup")) return "__tc_PinGroup";
761
+ const fnTypeMatch = typeName.match(/^std::function<\s*([^()<>]+)\((.*)\)\s*>$/);
762
+ if (fnTypeMatch) {
763
+ const returnType = fnTypeMatch[1].trim();
764
+ const params = fnTypeMatch[2].trim();
765
+ return `${returnType} (*)(${params})`;
766
+ }
767
+ return typeName;
768
+ }
769
+ mapReturnType(functionName: string, returnType: string): string {
770
+ if (functionName === "setup" || functionName === "loop") return "void";
771
+ return this.normalizeCppType(returnType);
772
+ }
773
+ isStringLikeType(cppType: string): boolean {
774
+ return cppType === "const char*" || cppType === "char*" || cppType === "String" || cppType === "__tc_str_ptr" || cppType === "std::string";
775
+ }
776
+ isPointerType(cppType: string): boolean {
777
+ return cppType.endsWith("*");
778
+ }
779
+ mapFunctionName(originalName: string): string {
780
+ if (originalName === "void" || originalName === "__cuttlefish_entrypoint__") return "setup";
781
+ if (originalName === "main") return "cuttlefish_main";
782
+ return originalName;
783
+ }
784
+
785
+ // ── Expression rendering ────────────────────────────────────────────────
786
+
787
+ currentTimeMillis(): string { return "millis()"; }
788
+
789
+ // Apply regex transformations to raw expression text.
790
+ // NOTE: String method regexes (toUpperCase, includes, etc.) assume the receiver
791
+ // is a string (const char*). Array methods (indexOf, push, etc.) are handled at
792
+ // the IR level in expression-to-ir.ts where type context is available.
793
+ normalizeRawExpression(value: string): string {
794
+ let v = value
795
+ .replace(/===/g, "==")
796
+ .replace(/!==/g, "!=")
797
+ .replace(/\?\?/g, "/* ?? */");
798
+ v = v.replace(/\bPinMode::(HIGH|LOW|INPUT|OUTPUT|INPUT_PULLUP)\b/g, "PinMode::_$1");
799
+ v = v.replace(/\bPinMode::(_?[A-Z_]+)\b/g, "static_cast<int>(PinMode::$1)");
800
+ v = v.replace(/(->|\.)capabilities\.interrupt\b/g, "$1capabilities");
801
+ v = v.replace(
802
+ /\bstd::(floor|ceil|round|trunc|sqrt|pow|sin|cos|tan|asin|acos|atan|abs|max|min)\b/g,
803
+ "$1",
804
+ );
805
+ v = v.replace(/\bDate\.now\(\)/g, "millis()");
806
+ v = v.replace(/\bundefined\b/g, "CUTTLEFISH_UNDEFINED");
807
+ v = v.replace(/\bnull\b/g, "CUTTLEFISH_UNDEFINED");
808
+
809
+ // String-method lowering is shared across all targets (see
810
+ // string-method-registry). Arduino special-cases includes/startsWith
811
+ // (strstr/strncmp) and wraps the indexOf receiver in __tc_str_ptr.
812
+ v = applyStringMethodRewrites(v, {
813
+ wrapReceiverFor: new Set(["indexOf"]),
814
+ special: {
815
+ includes: (recv, args) => `(strstr(${recv}, ${args[0]}) != NULL)`,
816
+ startsWith: (recv, args) => `(strncmp(${recv}, ${args[0]}, strlen(${args[0]})) == 0)`,
817
+ },
818
+ });
819
+
820
+ // Timer transformations are now handled via HAL resolver in timing.ts
821
+
822
+ if (this._usesPinGroup) {
823
+ v = v.replace(/createPinGroup\(\{\s*(.*?)\s*\}\)/g, '__tc_createPinGroup($1)');
824
+ }
825
+
826
+ // Prevent macro expansion for Num methods (abs, min, max, map, constrain).
827
+ // The __tc_Num shim declares these with a leading underscore (_abs, _map,
828
+ // …). escapeCppKeyword may have already mangled the call site to a trailing
829
+ // underscore (abs_, min_, max_) because abs/min/max collide with C/Arduino
830
+ // macros, so match both the raw and the escaped forms.
831
+ v = v.replace(/Num\.(abs|min|max|map|constrain)_?\(/g, "Num._$1(");
832
+
833
+ return v;
834
+ }
835
+ nullValue(): string {
836
+ return "CUTTLEFISH_UNDEFINED";
837
+ }
838
+ wrapStringConcat(leftRendered: string, rightRendered: string, leftIsString: boolean): string | undefined {
839
+ // When snprintf mode is active, string concat is handled at the expression
840
+ // renderer level — no String() wrapping needed here.
841
+ if (this.useSnprintfForStrings()) {
842
+ return undefined;
843
+ }
844
+ if (leftIsString) {
845
+ return `String(${leftRendered}) + ${rightRendered}`;
846
+ }
847
+ return undefined;
848
+ }
849
+ wrapStringObject(value: string): string {
850
+ return `__tc_str_ptr(${value})`;
851
+ }
852
+ useSnprintfForStrings(): boolean {
853
+ return true;
854
+ }
855
+ floatToSnprintfArg(
856
+ renderedExpr: string,
857
+ precision: number | undefined,
858
+ tempId: number,
859
+ ): { format: string; arg: string; estimatedLength: number; preludeLines: string[] } | undefined {
860
+ // Only use dtostrf on AVR where snprintf %f is disabled to save flash space.
861
+ // On ESP32, SAMD, and other modern architectures, snprintf supports %f natively.
862
+ if (this._cachedArch !== 'avr') {
863
+ return undefined;
864
+ }
865
+
866
+ const effectivePrecision = precision ?? 6;
867
+ const bufferName = `__cuttlefish_float_${tempId}`;
868
+ const estimatedLength = Math.max(16, effectivePrecision + 8);
869
+ return {
870
+ format: "%s",
871
+ arg: bufferName,
872
+ estimatedLength,
873
+ preludeLines: [
874
+ `char ${bufferName}[${estimatedLength}];`,
875
+ `dtostrf(${renderedExpr}, 0, ${effectivePrecision}, ${bufferName});`,
876
+ ],
877
+ };
878
+ }
879
+ renameEnumMember(_enumName: string, memberName: string): string {
880
+ return ARDUINO_ENUM_MEMBER_RENAMES.has(memberName) ? `_${memberName}` : memberName;
881
+ }
882
+ enumCastType(enumName: string): string | undefined {
883
+ return undefined;
884
+ }
885
+ private static readonly _passthroughEnumNames = new Set(["AnalogReference"]);
886
+ passthroughEnumNames(): ReadonlySet<string> {
887
+ return ArduinoStrategy._passthroughEnumNames;
888
+ }
889
+ renderBoardDefinitionAccess(
890
+ chain: string[],
891
+ boardConstants?: BoardConstants,
892
+ ): string | undefined {
893
+ if (chain.length < 3) return undefined;
894
+ if (chain[0] !== "Board" && chain[0] !== "Pins") return undefined;
895
+ if (chain[1] !== "definition") return undefined;
896
+ if (!boardConstants) return undefined;
897
+
898
+ const path = chain.slice(2).join(".");
899
+ const val = boardConstants.get(path);
900
+ return val !== undefined ? String(val) : undefined;
901
+ }
902
+
903
+ mapPeripheralIdentifier(name: string): string | undefined {
904
+ const canonical = name.toUpperCase();
905
+ if (canonical === "UART0") return "Serial";
906
+ if (canonical === "I2C0") return "Wire";
907
+ if (canonical === "SPI0") return "SPI";
908
+ return undefined;
909
+ }
910
+
911
+ resolvePinType(objectName: string, fieldName: string): string | undefined {
912
+ if (objectName !== "Pins") return undefined;
913
+ // AVR port-name patterns (ATmega328P)
914
+ if (fieldName === "PD2") return "AVRInterruptPin*";
915
+ if (fieldName === "PD3") return "AVRPWMInterruptPin*";
916
+ if (["PD5", "PD6", "PB1", "PB2", "PB3"].includes(fieldName)) return "AVRPWMPin*";
917
+ if (/^PC\d+$/.test(fieldName)) return "AVRAnalogPin*";
918
+ if (/^P[A-L]\d+$/.test(fieldName) || fieldName === "LED") return "AVRDigitalPin*";
919
+ // ESP32 GPIO-name patterns
920
+ if (/^GPIO\d+$/.test(fieldName) || fieldName === "LED") return "int";
921
+ return undefined;
922
+ }
923
+
924
+ // ── Statement rendering ─────────────────────────────────────────────────
925
+
926
+ renderThrow(_valueExpr: string): string {
927
+ return "cuttlefish_halt(\"PANIC\")";
928
+ }
929
+ isConsoleCall(callee: string): boolean {
930
+ return callee.startsWith("console.");
931
+ }
932
+ transformConsoleCall(method: string, renderedArgs: string, forHeader: boolean): string {
933
+ const semi = forHeader ? "" : ";";
934
+ // Wrap bare string literals with F() to store them in program memory
935
+ const isBareLiteral = /^"[^"]*"$/.test(renderedArgs);
936
+ const safeArgs = isBareLiteral ? `F(${renderedArgs})` : renderedArgs;
937
+
938
+ // Multi-arg console.log joins args with `<<`, but Serial.println takes a
939
+ // single value (no operator<< on HardwareSerial). Split the chain into a
940
+ // sequence of Serial.print(...) calls ending with Serial.println() so
941
+ // mixed-type args ("label", number) compile and print on one line.
942
+ const isChain = !isBareLiteral && renderedArgs.includes("<<");
943
+ let prefix = "";
944
+ switch (method) {
945
+ case "log":
946
+ break;
947
+ case "error":
948
+ prefix = `Serial.print(F("[ERROR] "))${semi} `;
949
+ break;
950
+ case "warn":
951
+ prefix = `Serial.print(F("[WARN] "))${semi} `;
952
+ break;
953
+ case "info":
954
+ prefix = `Serial.print(F("[INFO] "))${semi} `;
955
+ break;
956
+ case "debug":
957
+ prefix = `Serial.print(F("[DEBUG] "))${semi} `;
958
+ break;
959
+ default:
960
+ break;
961
+ }
962
+ if (isChain) {
963
+ // Split "a" << b << c into ["a", "b", "c"] (respecting string literals).
964
+ const parts = splitStreamChain(renderedArgs);
965
+ const prints = parts.map((p, i) => {
966
+ const last = i === parts.length - 1;
967
+ return last
968
+ ? `Serial.println(${wrapArg(p)})${semi}`
969
+ : `Serial.print(${wrapArg(p)})${semi}`;
970
+ });
971
+ return prefix + prints.join(" ");
972
+ }
973
+ return `${prefix}Serial.println(${safeArgs})${semi}`;
974
+ }
975
+
976
+ transformConsoleExpression(_method: string, _renderedArgs: string): string | undefined {
977
+ return undefined;
978
+ }
979
+ objectFieldInitializer(fieldValue: ExpressionIR, _renderExpr: (e: ExpressionIR) => string): string | undefined {
980
+ // Nested objects are now supported with proper nested struct definitions
981
+ return undefined;
982
+ }
983
+ overrideClassFieldType(fieldName: string, normalizedType: string): string {
984
+ // _interruptHandler is a function pointer on Arduino
985
+ if (fieldName === "_interruptHandler" && normalizedType === "int") {
986
+ return "void (*)(void)";
987
+ }
988
+ return normalizedType;
989
+ }
990
+
991
+ renderSerialPrintWithSnprintf(params: {
992
+ receiver: string;
993
+ method: string;
994
+ bufferName: string;
995
+ }): { finalLine: string } | undefined {
996
+ const serialInstance = params.receiver.startsWith("UART")
997
+ ? params.receiver.slice(4) === "0" ? "Serial" : `Serial${params.receiver.slice(4)}`
998
+ : params.receiver.startsWith("Serial")
999
+ ? params.receiver
1000
+ : "Serial";
1001
+ return {
1002
+ finalLine: `${serialInstance}.${params.method}(${params.bufferName});`,
1003
+ };
1004
+ }
1005
+
1006
+ // ── Name guards ─────────────────────────────────────────────────────────
1007
+
1008
+ forwardDeclarationExclusions(): string[] {
1009
+ return ["setup", "loop", "cuttlefish_main"];
1010
+ }
1011
+
1012
+ ambientTypeDeclarations(): string[] {
1013
+ return [
1014
+ "",
1015
+ " // Timing utilities (transpiled to millis/micros/delay/delayMicroseconds)",
1016
+ " const Timing: {",
1017
+ " millis(): number;",
1018
+ " micros(): number;",
1019
+ " delay(ms: number): void;",
1020
+ " delayMicroseconds(us: number): void;",
1021
+ " };",
1022
+ "",
1023
+ " // EEPROM non-volatile storage (transpiled to EEPROM.*)",
1024
+ " const EEPROM: {",
1025
+ " read(addr: number): number;",
1026
+ " write(addr: number, value: number): void;",
1027
+ " update(addr: number, value: number): void;",
1028
+ " length(): number;",
1029
+ " get<T>(addr: number, ref: T): T;",
1030
+ " put<T>(addr: number, ref: T): void;",
1031
+ " };",
1032
+ "",
1033
+ " // Watchdog timer (transpiled to wdt_enable/wdt_reset/wdt_disable)",
1034
+ " const WDT: {",
1035
+ " enable(timeout?: '15ms' | '30ms' | '60ms' | '120ms' | '250ms' | '500ms' | '1s' | '2s' | '4s' | '8s'): void;",
1036
+ " reset(): void;",
1037
+ " disable(): void;",
1038
+ " };",
1039
+ "",
1040
+ " // Key-value non-volatile storage (EEPROM-backed on AVR, native Preferences.h on ESP32)",
1041
+ " const Preferences: {",
1042
+ " begin(name: string, readOnly?: boolean): void;",
1043
+ " end(): void;",
1044
+ " putInt(key: string, value: number): void;",
1045
+ " getInt(key: string, defaultValue: number): number;",
1046
+ " putUInt(key: string, value: number): void;",
1047
+ " getUInt(key: string, defaultValue: number): number;",
1048
+ " putBool(key: string, value: boolean): void;",
1049
+ " getBool(key: string, defaultValue: boolean): boolean;",
1050
+ " putFloat(key: string, value: number): void;",
1051
+ " getFloat(key: string, defaultValue: number): number;",
1052
+ " putString(key: string, value: string): void;",
1053
+ " getString(key: string, defaultValue: string): string;",
1054
+ " remove(key: string): void;",
1055
+ " };",
1056
+ // Augment the '@typecad/board' module to re-export ownership types so that
1057
+ // `import { Owned, Shared } from '@typecad/board'` resolves correctly during
1058
+ // the transpiler's pre-emit type-check. The strings below close the
1059
+ // enclosing `declare global {`, open a module augmentation, then
1060
+ // re-open `declare global {` for the caller's closing brace.
1061
+ "}",
1062
+ "declare module '@typecad/board' {",
1063
+ " export type Owned<T = any> = T;",
1064
+ " export type Shared<T = any> = T;",
1065
+ " export type Mutable<T = any> = T;",
1066
+ "}",
1067
+ "declare global {",
1068
+ " export type Owned<T = any> = T;",
1069
+ " export type Shared<T = any> = T;",
1070
+ " export type Mutable<T = any> = T;",
1071
+ "}",
1072
+ "declare global {",
1073
+ ];
1074
+ }
1075
+
1076
+ reservedNames(): ReadonlySet<string> {
1077
+ return ARDUINO_RESERVED_NAMES;
1078
+ }
1079
+ passthroughMacroNames(): ReadonlySet<string> {
1080
+ return ARDUINO_PASSTHROUGH_MACROS;
1081
+ }
1082
+ apiReservedEnumNames(): ReadonlySet<string> {
1083
+ return ARDUINO_API_RESERVED_ENUMS;
1084
+ }
1085
+ apiReservedEnumGuard(): string {
1086
+ return "ARDUINO_API_VERSION";
1087
+ }
1088
+
1089
+ isHeapAllocationUnsafe(architecture: string): boolean {
1090
+ return architecture === 'avr' || architecture === 'megaavr';
1091
+ }
1092
+
1093
+ isExceptionSupportDisabled(architecture: string): boolean {
1094
+ return architecture === 'avr' || architecture === 'megaavr';
1095
+ }
1096
+
1097
+ // ── Includes ────────────────────────────────────────────────────────────
1098
+
1099
+ needsIostream(): boolean { return false; }
1100
+ needsStdString(): boolean { return false; }
1101
+ needsStdVector(): boolean { return false; }
1102
+ needsStdExcept(): boolean { return false; }
1103
+ needsStdFunction(): boolean { return false; }
1104
+ mathHeader(): string { return "<math.h>"; }
1105
+ cstringHeader(): string { return "<string.h>"; }
1106
+ needsVectorOverload(): boolean { return false; }
1107
+
1108
+ // ── Enum underlying type ────────────────────────────────────────────────
1109
+
1110
+ needsLargeEnumUnderlying(): boolean { return true; }
1111
+
1112
+ // ── Struct field handling ───────────────────────────────────────────────
1113
+
1114
+ renameStructField(fieldName: string): string {
1115
+ return ARDUINO_RESERVED_NAMES.has(fieldName) ? `_${fieldName}` : fieldName;
1116
+ }
1117
+ structFieldInitializer(
1118
+ fieldValue: ExpressionIR,
1119
+ compiletimeVarNames: Set<string>,
1120
+ _renderExpr: (e: ExpressionIR) => string,
1121
+ ): string | undefined {
1122
+ // Nested object literals are now supported with proper struct definitions
1123
+ // so they should render normally as aggregate initializers.
1124
+ if (fieldValue.kind === "identifier") {
1125
+ const identName = fieldValue.value;
1126
+ if (!compiletimeVarNames.has(identName)) return "0";
1127
+ }
1128
+ return undefined;
1129
+ }
1130
+
1131
+ // ── Async ───────────────────────────────────────────────────────────────
1132
+
1133
+ getAsyncRuntimeConfig(): AsyncRuntimeConfig {
1134
+ return {
1135
+ queueCapacity: 32,
1136
+ scheduler: "microtask",
1137
+ waitForPinEdge: "polling",
1138
+ hasPromiseRuntime: true,
1139
+ hasTimers: true,
1140
+ requiredIncludes: ["<functional>", "<vector>", "<utility>", "<string>"],
1141
+ };
1142
+ }
1143
+
1144
+ asyncLoopInjection(taskVarNames: string[], config: AsyncRuntimeConfig): string[];
1145
+ asyncLoopInjection(taskVarNames: string[], hasPromiseRuntime: boolean, hasTimers: boolean): string[];
1146
+ asyncLoopInjection(taskVarNames: string[], configOrBool: AsyncRuntimeConfig | boolean, hasTimers?: boolean): string[] {
1147
+ // Support both old (boolean) and new (AsyncRuntimeConfig) signatures
1148
+ let hasPromiseRuntime: boolean;
1149
+ let hasTimersVal: boolean;
1150
+ if (typeof configOrBool === 'boolean') {
1151
+ hasPromiseRuntime = configOrBool;
1152
+ hasTimersVal = hasTimers ?? false;
1153
+ } else {
1154
+ hasPromiseRuntime = configOrBool.hasPromiseRuntime;
1155
+ hasTimersVal = configOrBool.hasTimers;
1156
+ }
1157
+ const lines: string[] = [];
1158
+ for (const n of taskVarNames) {
1159
+ lines.push(` ${n}.run();`);
1160
+ }
1161
+ if (hasPromiseRuntime) lines.push(" cuttlefish_pump_microtasks();");
1162
+ if (hasTimersVal) lines.push(" __tc_timer_runtime.run();");
1163
+ return lines;
1164
+ }
1165
+ asyncDriverFunctionName(): string { return "loop"; }
1166
+
1167
+ /**
1168
+ * On ESP32 (Xtensa LX6/LX7), ISR functions must be placed in IRAM so they
1169
+ * can execute while the SPI flash cache is busy. Other architectures don't
1170
+ * need this attribute.
1171
+ */
1172
+ isrFunctionAttribute(): string {
1173
+ const arch = this._cachedArch;
1174
+ return (arch === 'esp32' || arch === 'esp32s2' || arch === 'esp32s3' || arch === 'esp32c3' || arch === 'esp32c6')
1175
+ ? 'IRAM_ATTR '
1176
+ : '';
1177
+ }
1178
+
1179
+ // ── Type aliases ────────────────────────────────────────────────────────
1180
+
1181
+ shouldSkipTypeAlias(cppType: string): boolean {
1182
+ return cppType.includes("std::string");
1183
+ }
1184
+
1185
+ // ── Diagnostics ─────────────────────────────────────────────────────────
1186
+
1187
+ emitDiagnostics(_emitMode: string): Diagnostic[] {
1188
+ // Split mode info message removed - Arduino inherently uses .ino format
1189
+ return [];
1190
+ }
1191
+
1192
+ // ── Interrupt safety ────────────────────────────────────────────────────
1193
+
1194
+ isrUnsafeOperations(): Map<string, { reason: string; severity: 'warning' | 'info' }> {
1195
+ return new Map([
1196
+ ['delay', { reason: 'delay() blocks the CPU and should not be used in interrupt context', severity: 'warning' }],
1197
+ ['delayMicroseconds', { reason: 'delayMicroseconds() blocks and should be avoided in ISRs', severity: 'warning' }],
1198
+ ['Serial.print', { reason: 'Serial.print() may not work correctly in interrupt context', severity: 'info' }],
1199
+ ['Serial.println', { reason: 'Serial.println() may not work correctly in interrupt context', severity: 'info' }],
1200
+ ['Serial.write', { reason: 'Serial.write() may not work correctly in interrupt context', severity: 'info' }],
1201
+ ['Serial.read', { reason: 'Serial.read() may not work correctly in interrupt context', severity: 'info' }],
1202
+ ['I2C0', { reason: 'I2C operations can cause lockups in interrupt context', severity: 'warning' }],
1203
+ ['I2C1', { reason: 'I2C operations can cause lockups in interrupt context', severity: 'warning' }],
1204
+ ['SPI0', { reason: 'SPI operations may cause issues in interrupt context', severity: 'info' }],
1205
+ ['SPI1', { reason: 'SPI operations may cause issues in interrupt context', severity: 'info' }],
1206
+ ]);
1207
+ }
1208
+
1209
+ // ── Build configuration ──────────────────────────────────────────────────
1210
+
1211
+ asyncQueueCapacity(): number { return 32; }
1212
+ outputSubdirectory(baseName: string): string { return baseName; }
1213
+ generateHeaderFile(): boolean { return false; }
1214
+ enumApiGuard(_enumName: string): { open: string; close: string } | undefined {
1215
+ return { open: "#if !defined(ARDUINO_API_VERSION)", close: "#endif // !defined(ARDUINO_API_VERSION)" };
1216
+ }
1217
+
1218
+ private static readonly STDLIB_SUPPORT: Record<string, StdLibSupport> = {
1219
+ avr: {
1220
+ hasVector: false, hasString: false, hasIostream: false,
1221
+ hasExceptions: false, hasRTTI: false,
1222
+ recommendedArrayImpl: "static_array", recommendedStringImpl: "static_string",
1223
+ },
1224
+ esp32: {
1225
+ hasVector: true, hasString: true, hasIostream: true,
1226
+ hasExceptions: true, hasRTTI: true,
1227
+ recommendedArrayImpl: "std_vector", recommendedStringImpl: "std_string",
1228
+ },
1229
+ esp8266: {
1230
+ hasVector: true, hasString: true, hasIostream: true,
1231
+ hasExceptions: true, hasRTTI: true,
1232
+ recommendedArrayImpl: "std_vector", recommendedStringImpl: "std_string",
1233
+ },
1234
+ rp2040: {
1235
+ hasVector: true, hasString: true, hasIostream: true,
1236
+ hasExceptions: true, hasRTTI: true,
1237
+ recommendedArrayImpl: "std_vector", recommendedStringImpl: "std_string",
1238
+ },
1239
+ samd: {
1240
+ hasVector: true, hasString: true, hasIostream: true,
1241
+ hasExceptions: true, hasRTTI: true,
1242
+ recommendedArrayImpl: "std_vector", recommendedStringImpl: "std_string",
1243
+ },
1244
+ megaavr: {
1245
+ hasVector: false, hasString: false, hasIostream: false,
1246
+ hasExceptions: false, hasRTTI: false,
1247
+ recommendedArrayImpl: "static_array", recommendedStringImpl: "static_string",
1248
+ },
1249
+ };
1250
+
1251
+ getStdLibSupport(architecture?: string): StdLibSupport {
1252
+ if (!architecture) return ArduinoStrategy.DEFAULT_STDLIB;
1253
+ return ArduinoStrategy.STDLIB_SUPPORT[architecture.toLowerCase()] ?? ArduinoStrategy.DEFAULT_STDLIB;
1254
+ }
1255
+
1256
+ private static readonly DEFAULT_STDLIB: StdLibSupport = {
1257
+ hasVector: true, hasString: true, hasIostream: true,
1258
+ hasExceptions: true, hasRTTI: true,
1259
+ recommendedArrayImpl: "std_vector", recommendedStringImpl: "std_string",
1260
+ };
1261
+
1262
+ // ── Debug code generation ─────────────────────────────────────────────────
1263
+
1264
+ generateDebugInitCode(): string[] {
1265
+ return generateSerialInitCode();
1266
+ }
1267
+
1268
+ generateDebugBreakpointCode(params: {
1269
+ fileName: string; lineNum: number; originalLine: string;
1270
+ variables: Array<{ name: string; isFunction?: boolean }>;
1271
+ normalizedCondition?: string;
1272
+ }): string[] {
1273
+ return generateBreakpointCode(
1274
+ params.fileName, params.lineNum, params.originalLine,
1275
+ params.variables, params.normalizedCondition,
1276
+ );
1277
+ }
1278
+
1279
+ generateDebugLogpointCode(params: {
1280
+ fileName: string; lineNum: number;
1281
+ parts: Array<{ type: 'text' | 'variable'; value: string }>;
1282
+ variables: Array<{ name: string; isFunction?: boolean }>;
1283
+ }): string[] {
1284
+ return generateLogpointCode(params.fileName, params.lineNum, params.parts, params.variables);
1285
+ }
1286
+
1287
+ // ── HAL Operation Resolution ────────────────────────────────────────────
1288
+
1289
+ resolveHALOperation(op: HALOpIR): { code?: string; expression?: string } | undefined {
1290
+ switch (op.operation) {
1291
+ // GPIO
1292
+ case "gpio.write": {
1293
+ // Literal 0/1 → HIGH/LOW; runtime expression → ternary coercion
1294
+ const v = op.value;
1295
+ const rhs = typeof v === "string" ? `(${v}) ? HIGH : LOW` : (v ? "HIGH" : "LOW");
1296
+ return { code: `digitalWrite(${op.pin}, ${rhs});` };
1297
+ }
1298
+ case "gpio.read":
1299
+ return { expression: `digitalRead(${op.pin})` };
1300
+ case "gpio.toggle":
1301
+ return { code: `digitalWrite(${op.pin}, digitalRead(${op.pin}) == LOW ? HIGH : LOW);` };
1302
+ case "gpio.set_mode":
1303
+ return { code: `pinMode(${op.pin}, ${op.mode});` };
1304
+
1305
+ // PWM
1306
+ case "pwm.write":
1307
+ return { code: `analogWrite(${op.pin}, ${op.duty});` };
1308
+ case "pwm.get_frequency":
1309
+ return { expression: `0` };
1310
+ case "pwm.get_resolution":
1311
+ return { expression: `8` };
1312
+
1313
+ // ADC
1314
+ case "adc.read":
1315
+ return { expression: `analogRead(${op.pin})` };
1316
+ case "adc.get_resolution":
1317
+ return { expression: `10` };
1318
+ case "adc.set_reference": {
1319
+ // Map known string references to Arduino macros; pass numeric values through.
1320
+ const refMap: Record<string, string> = {
1321
+ default: "DEFAULT",
1322
+ internal: "INTERNAL",
1323
+ internal1v1: "INTERNAL",
1324
+ external: "EXTERNAL",
1325
+ vdd: "DEFAULT",
1326
+ };
1327
+ // Strip surrounding quotes if present (HAL resolver may pass the raw
1328
+ // string literal text including quotes).
1329
+ const refStr = String(op.reference).replace(/^["']|["']$/g, "");
1330
+ const arduinoRef = refMap[refStr.toLowerCase()] ?? op.reference;
1331
+ return { code: `analogReference(${arduinoRef});` };
1332
+ }
1333
+ case "adc.get_reference":
1334
+ return { expression: `AR_DEFAULT` };
1335
+ case "adc.read_voltage": {
1336
+ const vRef = op.vRef ?? 5.0;
1337
+ const maxValue = op.maxValue ?? 1023.0;
1338
+ return { expression: `(analogRead(${op.pin}) * ${vRef} / ${maxValue})` };
1339
+ }
1340
+
1341
+ // DAC
1342
+ case "dac.write":
1343
+ return { code: `dacWrite(${op.pin}, ${op.value});` };
1344
+
1345
+ // Interrupts
1346
+ case "interrupt.attach": {
1347
+ // Mode arrives as an enum/string ("FALLING", "RISING", ...); strip any
1348
+ // surrounding quotes so it emits the unquoted Arduino macro.
1349
+ const intMode = String(op.mode).replace(/^["']|["']$/g, "").toUpperCase();
1350
+ return { code: `attachInterrupt(digitalPinToInterrupt(${op.pin}), ${op.handler}, ${intMode});` };
1351
+ }
1352
+ case "interrupt.detach":
1353
+ return { code: `detachInterrupt(digitalPinToInterrupt(${op.pin}));` };
1354
+
1355
+ // Tone
1356
+ case "tone.play":
1357
+ if (op.duration !== undefined) {
1358
+ return { code: `tone(${op.pin}, ${op.frequency}, ${op.duration});` };
1359
+ }
1360
+ return { code: `tone(${op.pin}, ${op.frequency});` };
1361
+ case "tone.stop":
1362
+ return { code: `noTone(${op.pin});` };
1363
+
1364
+ // Timing
1365
+ case "timing.delay":
1366
+ return { code: `delay(${op.ms});` };
1367
+ case "timing.delay_microseconds":
1368
+ return { code: `delayMicroseconds(${op.us});` };
1369
+ case "timing.millis":
1370
+ return { expression: `millis()` };
1371
+ case "timing.micros":
1372
+ return { expression: `micros()` };
1373
+ case "timing.free_heap":
1374
+ // Delegate to the architecture-aware freeHeap() method on the
1375
+ // __tc_Timing polyfill (emitted by shimLines()): ESP.getFreeHeap() on
1376
+ // ESP32, the __heap_start/__brkval trick on AVR, 0 elsewhere.
1377
+ return { expression: `Timing.freeHeap()` };
1378
+ case "timing.set_interval":
1379
+ case "timing.set_timeout":
1380
+ case "timing.clear_interval":
1381
+ case "timing.clear_timeout":
1382
+ return undefined;
1383
+
1384
+ // I2C
1385
+ case "i2c.begin":
1386
+ return { code: `${op.bus}.begin(${op.address ?? ""});` };
1387
+ case "i2c.end":
1388
+ return { code: `${op.bus}.end();` };
1389
+ case "i2c.set_clock":
1390
+ return { code: `${op.bus}.setClock(${op.hz});` };
1391
+ case "i2c.begin_transmission":
1392
+ return { code: `${op.bus}.beginTransmission(${op.address});` };
1393
+ case "i2c.write":
1394
+ return { code: `${op.bus}.write(${op.data});` };
1395
+ case "i2c.write_bytes":
1396
+ return { code: op.bytes.map((b) => `${op.bus}.write(${b});`).join("\n") };
1397
+ case "i2c.write_buffer":
1398
+ return { code: `${op.bus}.write(${op.data}, sizeof(${op.data}));` };
1399
+ case "i2c.read_buffer": {
1400
+ const target = op.buffer === "__HAL_READ_BUF__" ? "__DISCARD__" : op.buffer;
1401
+ if (target === "__DISCARD__") {
1402
+ return { code: `for (int __i = 0; __i < ${op.count}; __i++) (void)${op.bus}.read();` };
1403
+ }
1404
+ return { code: `for (int __i = 0; __i < ${op.count}; __i++) ${target}[__i] = ${op.bus}.read();` };
1405
+ }
1406
+ case "i2c.end_transmission":
1407
+ return { code: `${op.bus}.endTransmission(${op.stop ? "true" : "false"});` };
1408
+ case "i2c.request_from":
1409
+ return { code: `${op.bus}.requestFrom(${op.address}, ${op.quantity}, ${op.stop ? "true" : "false"});` };
1410
+ case "i2c.available":
1411
+ return { expression: `${op.bus}.available()` };
1412
+ case "i2c.read":
1413
+ return { expression: `${op.bus}.read()` };
1414
+ case "i2c.recover":
1415
+ return undefined;
1416
+
1417
+ // SPI
1418
+ case "spi.begin":
1419
+ return { code: `${op.bus}.begin();` };
1420
+ case "spi.end":
1421
+ return { code: `${op.bus}.end();` };
1422
+ case "spi.transfer":
1423
+ return { expression: `${op.bus}.transfer(${op.data})` };
1424
+ case "spi.begin_transaction":
1425
+ return { code: `${op.bus}.beginTransaction(${op.settings});` };
1426
+ case "spi.end_transaction":
1427
+ return { code: `${op.bus}.endTransaction();` };
1428
+ case "spi.set_mode":
1429
+ return { code: `${op.bus}.setDataMode(${op.mode});` };
1430
+ case "spi.set_bit_order":
1431
+ return { code: `${op.bus}.setBitOrder(${normalizeBitOrder(op.order)});` };
1432
+ case "spi.cs_low":
1433
+ return { code: `digitalWrite(${op.pin}, LOW);` };
1434
+ case "spi.cs_high":
1435
+ return { code: `digitalWrite(${op.pin}, HIGH);` };
1436
+
1437
+ // UART
1438
+ case "uart.begin":
1439
+ return { code: `${op.port}.begin(${op.baud});` };
1440
+ case "uart.end":
1441
+ return { code: `${op.port}.end();` };
1442
+ case "uart.print":
1443
+ return { code: `${op.port}.print(${op.value});` };
1444
+ case "uart.println":
1445
+ return { code: `${op.port}.println(${op.value});` };
1446
+ case "uart.printf":
1447
+ return { code: `${op.port}.printf(${op.format}, ${op.args.join(", ")});` };
1448
+ case "uart.write":
1449
+ return { code: `${op.port}.write(${op.data});` };
1450
+ case "uart.read":
1451
+ return { expression: `${op.port}.read()` };
1452
+ case "uart.peek":
1453
+ return { expression: `${op.port}.peek()` };
1454
+ case "uart.available":
1455
+ return { expression: `${op.port}.available()` };
1456
+ case "uart.flush":
1457
+ return { code: `${op.port}.flush();` };
1458
+
1459
+ // Pulse
1460
+ case "pulse.in":
1461
+ return { expression: `pulseIn(${op.pin}, ${op.value ? "HIGH" : "LOW"}${op.timeout !== undefined ? `, ${op.timeout}` : ""})` };
1462
+ case "pulse.in_long":
1463
+ return { expression: `pulseInLong(${op.pin}, ${op.value ? "HIGH" : "LOW"})` };
1464
+
1465
+ // Shift
1466
+ case "shift.out":
1467
+ return { code: `shiftOut(${op.dataPin}, ${op.clockPin}, ${normalizeBitOrder(op.bitOrder)}, ${op.value});` };
1468
+ case "shift.in":
1469
+ return { expression: `shiftIn(${op.dataPin}, ${op.clockPin}, ${normalizeBitOrder(op.bitOrder)})` };
1470
+
1471
+ // Board constants
1472
+ case "board.resolve":
1473
+ return { expression: this.renderBoardDefinitionAccess(op.path.split("."), undefined) };
1474
+
1475
+ // Watchdog timer — the avr/wdt.h symbols (wdt_enable/wdt_reset/wdt_disable
1476
+ // and the WDTO_* macros) are AVR-only. On ESP32-family architectures they
1477
+ // are undefined and would not compile, so emit a no-op comment there.
1478
+ // (The backing __tc_WDT struct at preamble-emit time is likewise AVR-only.)
1479
+ case "wdt.enable": {
1480
+ const arch = this._cachedArch;
1481
+ if (arch === 'esp32' || arch === 'esp32s2' || arch === 'esp32s3' || arch === 'esp32c3' || arch === 'esp32c6') {
1482
+ return { code: `// watchdog not supported on ${arch}` };
1483
+ }
1484
+ return { code: `wdt_enable(${normalizeWdtTimeout(op.timeout)});` };
1485
+ }
1486
+ case "wdt.reset": {
1487
+ const arch = this._cachedArch;
1488
+ if (arch === 'esp32' || arch === 'esp32s2' || arch === 'esp32s3' || arch === 'esp32c3' || arch === 'esp32c6') {
1489
+ return { code: `// watchdog not supported on ${arch}` };
1490
+ }
1491
+ return { code: `wdt_reset();` };
1492
+ }
1493
+ case "wdt.disable": {
1494
+ const arch = this._cachedArch;
1495
+ if (arch === 'esp32' || arch === 'esp32s2' || arch === 'esp32s3' || arch === 'esp32c3' || arch === 'esp32c6') {
1496
+ return { code: `// watchdog not supported on ${arch}` };
1497
+ }
1498
+ return { code: `wdt_disable();` };
1499
+ }
1500
+
1501
+ // Power — ESP32-family deep/light sleep. AVR and other architectures
1502
+ // lack these ESP-IDF symbols, so emit a not-supported comment there.
1503
+ case "power.deep_sleep": {
1504
+ const arch = this._cachedArch;
1505
+ if (arch === 'esp32' || arch === 'esp32s2' || arch === 'esp32s3' || arch === 'esp32c3' || arch === 'esp32c6') {
1506
+ return { code: `esp_sleep_enable_timer_wakeup(${op.ms} * 1000); esp_deep_sleep_start();` };
1507
+ }
1508
+ return { code: `// deep sleep not supported on ${arch}` };
1509
+ }
1510
+ case "power.light_sleep": {
1511
+ const arch = this._cachedArch;
1512
+ if (arch === 'esp32' || arch === 'esp32s2' || arch === 'esp32s3' || arch === 'esp32c3' || arch === 'esp32c6') {
1513
+ return { code: `esp_light_sleep_start();` };
1514
+ }
1515
+ return { code: `// light sleep not supported on ${arch}` };
1516
+ }
1517
+ case "power.set_cpu_frequency":
1518
+ return { code: `setCpuFrequencyMhz(${op.mhz});` };
1519
+
1520
+ // Snprintf
1521
+ case "snprintf.emit":
1522
+ return { code: `snprintf(${op.bufferName}, sizeof(${op.bufferName}), ${op.format}, ${op.args.join(", ")});` };
1523
+
1524
+ // Raw passthrough
1525
+ case "raw":
1526
+ return { code: op.code };
1527
+
1528
+ // Watchdog timer. The __tc_WDT struct's enable(const char*) overload keeps
1529
+ // a strcmp chain as a fallback for DYNAMIC timeout strings (a runtime
1530
+ // variable). For the common case — a literal preset like "250ms" — fold it
1531
+ // to the matching WDTO_* macro here at transpile time, emitting the exact
1532
+ // wdt_enable(WDTO_250MS) call a hand-written sketch would use. Numeric
1533
+ // timeouts and unrecognized strings pass through unchanged.
1534
+ case "wdt.enable": {
1535
+ const wdtoMap: Record<string, string> = {
1536
+ "15ms": "WDTO_15MS", "30ms": "WDTO_30MS", "60ms": "WDTO_60MS",
1537
+ "120ms": "WDTO_120MS", "250ms": "WDTO_250MS", "500ms": "WDTO_500MS",
1538
+ "1s": "WDTO_1S", "2s": "WDTO_2S", "4s": "WDTO_4S", "8s": "WDTO_8S",
1539
+ };
1540
+ const raw = String(op.timeout);
1541
+ // Strip surrounding quotes the HAL resolver may include for literals.
1542
+ const preset = raw.replace(/^["']|["']$/g, "");
1543
+ const macro = wdtoMap[preset];
1544
+ if (macro) return { code: `wdt_enable(${macro});` };
1545
+ return { code: `wdt_enable(${raw});` };
1546
+ }
1547
+ case "wdt.reset":
1548
+ return { code: `wdt_reset();` };
1549
+ case "wdt.disable":
1550
+ return { code: `wdt_disable();` };
1551
+
1552
+ default:
1553
+ return undefined;
1554
+ }
1555
+ }
1556
+
1557
+ // ── Graphics ───────────────────────────────────────────────────────────
1558
+ // Display context captured from display.init so later fill_rect/draw_text
1559
+ // calls address the right bus/pins. ILI9341 is the v1 driver; SSD1306 and
1560
+ // others are fast-follows (see DISPLAYS.md).
1561
+ private _displayCtx: ILI9341Context | null = null;
1562
+
1563
+ resolveDisplayOp(op: DisplayHALOp): { code?: string; expression?: string } | undefined {
1564
+ if (op.operation === "display.init") {
1565
+ const dop = op as any;
1566
+ this._displayCtx = {
1567
+ bus: op.bus, cs: op.cs, dc: op.dc, rst: op.rst,
1568
+ width: op.width, height: op.height,
1569
+ rotation: dop.rotation ?? 1,
1570
+ // Backlight pin: pass through unchanged. Emitting a pinMode for an
1571
+ // unset backlight would seize an arbitrary GPIO — historically this
1572
+ // defaulted to 17, which collided with the ST7796S DC pin on boards
1573
+ // that wire DC to GPIO 17. Only emit the backlight sequence when the
1574
+ // config explicitly declares a backlight pin.
1575
+ backlight: dop.backlight,
1576
+ spiFrequency: dop.spiFrequency,
1577
+ };
1578
+ }
1579
+ // Only resolve once a display is initialized (display.init sets the context).
1580
+ if (!this._displayCtx) return undefined;
1581
+ return resolveILI9341Op(op, this._displayCtx);
1582
+ }
1583
+
1584
+ supportedDisplayDrivers(): ReadonlySet<string> {
1585
+ return new Set(["ili9341", "st7796", "ssd1680", "ssd1309"]);
1586
+ }
1587
+
1588
+ colorFormat(): "rgb565" | "mono" {
1589
+ return "rgb565";
1590
+ }
1591
+
1592
+ graphicsCapacity(): GraphicsCapacity {
1593
+ // AVR profile (specified for completeness; the PoC ILI9341 driver is
1594
+ // ESP32-class/color, so AVR is not exercised by the PoC — but capacity
1595
+ // is returned so mount-time maxNodes diagnostics work uniformly).
1596
+ return {
1597
+ maxNodes: 256,
1598
+ maxBindings: 64,
1599
+ maxActiveTransitions: 32,
1600
+ nodeStorage: "progmem",
1601
+ };
1602
+ }
1603
+ }
1604
+
1605
+ // ---------------------------------------------------------------------------
1606
+ // Module-level helpers used by ArduinoStrategy
1607
+ // ---------------------------------------------------------------------------
1608
+
1609
+ /**
1610
+ * On no-`std::vector` architectures (AVR/megaAVR), surface a clear,
1611
+ * source-located error for every storage site whose type lowers to
1612
+ * `std::vector<T>` — class FIELDS, function PARAMETERS, and function RETURN
1613
+ * TYPES. A function-LOCAL array initialized from a literal is exempt: it
1614
+ * lowers to a fixed-size `__tc_StaticArray<T,N>` (the literal supplies N).
1615
+ *
1616
+ * This is the family-wide gate for "dynamically-grown array storage is not
1617
+ * supportable on a no-heap / no-STL target". Without it the transpiler emits
1618
+ * `std::vector<T>` verbatim and the user sees an opaque avr-g++ error
1619
+ * ("'vector' in namespace 'std' does not name a template type").
1620
+ *
1621
+ * Reused across the three storage classes so the rule stays consistent: any
1622
+ * `T[]` / `Array<T>` / `ReadonlyArray<T>` that resolves to `std::vector<T>`
1623
+ * (via parsedIsVector) and lives in a non-local storage slot is rejected.
1624
+ */
1625
+ function collectNoVectorStorageDiagnostics(program: ProgramIR): Diagnostic[] {
1626
+ const diagnostics: Diagnostic[] = [];
1627
+
1628
+ const vectorError = (site: string, typeName: string, span?: { filePath: string; startLine: number; startColumn: number }): Diagnostic => ({
1629
+ severity: "error" as const,
1630
+ code: "TS2CPP_NO_VECTOR_STORAGE",
1631
+ message:
1632
+ `${site} of type '${typeName}' lowers to 'std::vector<...>', which is not available on this target ` +
1633
+ `(ATmega AVR has no <vector> and discourages heap allocation). Dynamically-grown array storage ` +
1634
+ `cannot lower to the target's fixed-size '__tc_StaticArray<T,N>' (no compile-time size is recoverable).`,
1635
+ hint:
1636
+ "Use a function-local array (initialized from a literal — it lowers to a fixed-size buffer), " +
1637
+ "a Map/Set for keyed storage, or a fixed-shape interface/struct field. See SUPPORT_MATRIX §1.5 (AVR note).",
1638
+ line: span?.startLine,
1639
+ column: span?.startColumn,
1640
+ source: span?.filePath,
1641
+ });
1642
+
1643
+ // Class fields — `class C { data: int32_t[]; }`.
1644
+ for (const cls of program.classes) {
1645
+ for (const field of cls.fields) {
1646
+ // A field with a literal initializer is still typed by its ANNOTATION
1647
+ // (which resolves to std::vector), so the initializer does not rescue
1648
+ // it. Only the declared type matters here.
1649
+ if (parsedIsVector(field.cppType)) {
1650
+ diagnostics.push(vectorError(
1651
+ `Class field '${cls.name}.${field.name}'`,
1652
+ field.cppType,
1653
+ { filePath: cls.sourceSpan.filePath, startLine: cls.sourceSpan.startLine, startColumn: cls.sourceSpan.startColumn },
1654
+ ));
1655
+ }
1656
+ }
1657
+ }
1658
+
1659
+ // Free-function parameters and return types.
1660
+ for (const fn of program.functions) {
1661
+ for (const param of fn.parameters) {
1662
+ if (parsedIsVector(param.cppType)) {
1663
+ diagnostics.push(vectorError(
1664
+ `Parameter '${fn.originalName}(${param.name})'`,
1665
+ param.cppType,
1666
+ { filePath: fn.sourceSpan.filePath, startLine: fn.sourceSpan.startLine, startColumn: fn.sourceSpan.startColumn },
1667
+ ));
1668
+ }
1669
+ }
1670
+ if (parsedIsVector(fn.returnType)) {
1671
+ diagnostics.push(vectorError(
1672
+ `Return type of '${fn.originalName}'`,
1673
+ fn.returnType,
1674
+ { filePath: fn.sourceSpan.filePath, startLine: fn.sourceSpan.startLine, startColumn: fn.sourceSpan.startColumn },
1675
+ ));
1676
+ }
1677
+ }
1678
+
1679
+ return diagnostics;
1680
+ }
1681
+
1682
+ /**
1683
+ * Detect heap allocations (`new ClassName(...)`, `new Array<E>(n)`) anywhere in
1684
+ * the program — in var_decl initializers, assignments, returns, call arguments,
1685
+ * conditions, for-init, and nested sub-expressions — and surface a diagnostic
1686
+ * whose severity scales with the target.
1687
+ *
1688
+ * Detection keys off the `newClassName` marker a user-class `new` tags its raw
1689
+ * IR node with (set in `expression-to-ir.ts`), falling back to a text regex for
1690
+ * untagged / hand-built raw IR. `new Array<E>(n)` lowers to `std::vector<E>`
1691
+ * with no marker, so it is detected by substring on unsafe targets.
1692
+ *
1693
+ * Severity is target-parameterized via `unsafeTarget`:
1694
+ * - AVR/megaAVR (`unsafeTarget === true`): WARNING for `new` (the Arduino
1695
+ * core ships operator new/delete over avr-libc malloc/free — a real heap
1696
+ * manager — so `new` compiles, links, and runs; the only real concern is
1697
+ * the small heap ~1.5-1.8 KB on a Uno). ERROR for `std::vector` (avr-g++
1698
+ * has no <vector>, so `new Array<E>(n)` cannot link). (History: a prior
1699
+ * version of the `new` gate was a hard `error` claiming AVR had "no heap
1700
+ * manager" — factually wrong; demo #36 downgraded it to a warning.)
1701
+ * - Other targets (esp32, etc., `unsafeTarget === false`): INFO for `new`.
1702
+ * These targets have more RAM and a real heap, so a single allocation is
1703
+ * not inherently risky, but allocations inside loop() churn the heap over
1704
+ * time. No diagnostic for `std::vector` — it is valid C++ there.
1705
+ *
1706
+ * The walker inspects every expression-bearing field of every statement and
1707
+ * recurses into nested sub-expressions, so a `new` inside e.g.
1708
+ * `this.field = new Foo()`, `return cond ? new A() : new B()`, or
1709
+ * `process(new Foo())` is detected, not just top-level var_decl initializers.
1710
+ */
1711
+ function collectHeapAllocationDiagnostics(program: ProgramIR, arch: string, unsafeTarget: boolean): Diagnostic[] {
1712
+ const diagnostics: Diagnostic[] = [];
1713
+ const archUpper = arch.toUpperCase();
1714
+
1715
+ // Inspect a single raw IR node for heap allocation. A node is a heap `new`
1716
+ // when it carries the `newClassName` marker (set in expression-to-ir.ts for
1717
+ // user-class `new`) OR its text starts with `new `. On unsafe targets
1718
+ // (AVR/megaAVR), also flag `std::vector<` — the text `new Array<E>(n)`
1719
+ // lowers to — which AVR cannot host (avr-g++ has no <vector>).
1720
+ const checkRaw = (raw: { value: string; newClassName?: string }, stmt: StatementIR): void => {
1721
+ const tagged = raw.newClassName;
1722
+ const isHeapNew = !!tagged || /^new\s+\w/.test(raw.value);
1723
+ if (isHeapNew) {
1724
+ const match = raw.value.match(/^new\s+(\w+)/);
1725
+ const className = tagged ?? (match ? match[1] : "unknown");
1726
+ if (unsafeTarget) {
1727
+ diagnostics.push({
1728
+ severity: "warning" as const,
1729
+ code: "heap-allocation-avr",
1730
+ message:
1731
+ `Heap allocation (\`new ${className}()\`) on ${archUpper} uses the small ` +
1732
+ `runtime heap (~1.5-1.8 KB usable on a Uno). \`new\`/\`delete\` are supported by the ` +
1733
+ `Arduino core, but heavy or churning allocation risks fragmentation/exhaustion.`,
1734
+ hint:
1735
+ `This compiles and runs. For long-lived or frequently-allocated objects on a small-RAM ` +
1736
+ `target, consider a stack/global instance (auto ${className} obj(...);) or reusing a ` +
1737
+ `single allocation to avoid heap fragmentation.`,
1738
+ line: (stmt as { sourceSpan?: { startLine?: number } }).sourceSpan?.startLine,
1739
+ column: (stmt as { sourceSpan?: { startColumn?: number } }).sourceSpan?.startColumn,
1740
+ source: "framework-arduino",
1741
+ });
1742
+ } else {
1743
+ diagnostics.push({
1744
+ severity: "info" as const,
1745
+ code: "heap-allocation",
1746
+ message:
1747
+ `Heap allocation (\`new ${className}()\`) on ${archUpper}. This target has more RAM and ` +
1748
+ `a real heap manager, so a single allocation is not inherently risky, but allocations ` +
1749
+ `inside loop() churn the heap over time and can fragment it on long-running sketches.`,
1750
+ hint:
1751
+ `For large or frequent buffers, prefer PSRAM when available (` +
1752
+ `display_createCanvasPsram / ps_malloc), or reuse a single long-lived allocation ` +
1753
+ `instead of allocating per-iteration.`,
1754
+ line: (stmt as { sourceSpan?: { startLine?: number } }).sourceSpan?.startLine,
1755
+ column: (stmt as { sourceSpan?: { startColumn?: number } }).sourceSpan?.startColumn,
1756
+ source: "framework-arduino",
1757
+ });
1758
+ }
1759
+ return;
1760
+ }
1761
+ // `new Array<E>(n)` lowers to `std::vector<E>(n)` as a raw node with no
1762
+ // marker and text that does not start with `new` — so it evades the check
1763
+ // above. On unsafe targets (AVR) this is a hard failure (no <vector>),
1764
+ // surfaced here as an error rather than an opaque downstream g++ message.
1765
+ if (unsafeTarget && raw.value.includes("std::vector<")) {
1766
+ diagnostics.push({
1767
+ severity: "error" as const,
1768
+ code: "heap-allocation-avr",
1769
+ message:
1770
+ `\`new Array<E>(n)\` lowers to \`std::vector<E>\`, which ${archUpper} cannot host ` +
1771
+ `(avr-g++ ships no <vector>).`,
1772
+ hint:
1773
+ `Use an array literal (\`const a: E[] = [0,0,...]\`) which lowers to a fixed-size ` +
1774
+ `__tc_StaticArray<E,N>, or declare a fixed-size buffer (\`E buf[N];\`).`,
1775
+ line: (stmt as { sourceSpan?: { startLine?: number } }).sourceSpan?.startLine,
1776
+ column: (stmt as { sourceSpan?: { startColumn?: number } }).sourceSpan?.startColumn,
1777
+ source: "framework-arduino",
1778
+ });
1779
+ }
1780
+ };
1781
+
1782
+ // Recursively walk an expression tree, checking every raw leaf. Mirrors the
1783
+ // shape of walkExpressionsInExpression in the cuttlefish ir/utils (not
1784
+ // exported through the api surface this strategy consumes), kept local so a
1785
+ // `new` nested inside e.g. `compute(new Foo())` or `cond ? new A() : new B()`
1786
+ // is still detected.
1787
+ const checkExpression = (expr: ExpressionIR | undefined, stmt: StatementIR): void => {
1788
+ if (!expr || typeof expr !== "object" || !(expr as { kind?: string }).kind) return;
1789
+ const e = expr as Record<string, unknown>;
1790
+ if (expr.kind === "raw") {
1791
+ checkRaw(expr as { value: string; newClassName?: string }, stmt);
1792
+ }
1793
+ if (Array.isArray(e.args)) {
1794
+ for (const a of e.args as ExpressionIR[]) checkExpression(a, stmt);
1795
+ }
1796
+ if (e.left) checkExpression(e.left as ExpressionIR, stmt);
1797
+ if (e.right) checkExpression(e.right as ExpressionIR, stmt);
1798
+ if (e.operand) checkExpression(e.operand as ExpressionIR, stmt);
1799
+ if (e.condition && typeof e.condition === "object") checkExpression(e.condition as ExpressionIR, stmt);
1800
+ if (e.whenTrue) checkExpression(e.whenTrue as ExpressionIR, stmt);
1801
+ if (e.whenFalse) checkExpression(e.whenFalse as ExpressionIR, stmt);
1802
+ if (e.inner) checkExpression(e.inner as ExpressionIR, stmt);
1803
+ if (e.object && typeof e.object === "object" && expr.kind !== "instanceof") {
1804
+ checkExpression(e.object as ExpressionIR, stmt);
1805
+ }
1806
+ if (e.value && typeof e.value === "object" && "kind" in (e.value as object)) {
1807
+ checkExpression(e.value as ExpressionIR, stmt);
1808
+ }
1809
+ if (Array.isArray(e.elements)) {
1810
+ for (const el of e.elements as ExpressionIR[]) checkExpression(el, stmt);
1811
+ }
1812
+ if (Array.isArray(e.fields)) {
1813
+ for (const f of e.fields as Array<{ value: ExpressionIR }>) checkExpression(f.value, stmt);
1814
+ }
1815
+ if (Array.isArray(e.parts)) {
1816
+ for (const p of e.parts as ExpressionIR[]) checkExpression(p, stmt);
1817
+ }
1818
+ if (e.expression && typeof e.expression === "object" && "kind" in (e.expression as object)) {
1819
+ checkExpression(e.expression as ExpressionIR, stmt);
1820
+ }
1821
+ // Lambda/callback bodies embed statements — recurse so a `new` inside an
1822
+ // arrow function is caught.
1823
+ if (Array.isArray(e.statements)) {
1824
+ for (const s of e.statements as StatementIR[]) visit([s]);
1825
+ }
1826
+ };
1827
+
1828
+ // Visit statements: check every expression-bearing field, then recurse into
1829
+ // nested compound bodies. This is statement-position-agnostic — a `new` in an
1830
+ // assignment, return, call arg, condition, or for-init is detected, not just
1831
+ // var_decl initializers.
1832
+ const visit = (stmts: StatementIR[]): void => {
1833
+ for (const stmt of stmts) {
1834
+ const s = stmt as unknown as Record<string, unknown>;
1835
+ if (s.initializer) checkExpression(s.initializer as ExpressionIR, stmt);
1836
+ if (s.value && typeof s.value === "object") checkExpression(s.value as ExpressionIR, stmt);
1837
+ if (s.condition && typeof s.condition === "object") checkExpression(s.condition as ExpressionIR, stmt);
1838
+ if (s.expression && typeof s.expression === "object") checkExpression(s.expression as ExpressionIR, stmt);
1839
+ if (Array.isArray(s.args)) {
1840
+ for (const a of s.args as ExpressionIR[]) checkExpression(a, stmt);
1841
+ }
1842
+ // for-of / for-in iterable/object
1843
+ if (s.iterable && typeof s.iterable === "object") checkExpression(s.iterable as ExpressionIR, stmt);
1844
+ if (s.object && typeof s.object === "object") checkExpression(s.object as ExpressionIR, stmt);
1845
+ // for-loop initializer/increment are statements
1846
+ if (s.initializer && typeof s.initializer === "object" && "kind" in (s.initializer as object)) {
1847
+ visit([s.initializer as StatementIR]);
1848
+ }
1849
+ if (s.increment && typeof s.increment === "object" && "kind" in (s.increment as object)) {
1850
+ visit([s.increment as StatementIR]);
1851
+ }
1852
+ // Recurse into nested compound bodies.
1853
+ if (Array.isArray(s.body)) visit(s.body as StatementIR[]);
1854
+ if (Array.isArray(s.thenBranch)) visit(s.thenBranch as StatementIR[]);
1855
+ if (Array.isArray(s.elseBranch)) visit(s.elseBranch as StatementIR[]);
1856
+ if (Array.isArray(s.tryBlock)) visit(s.tryBlock as StatementIR[]);
1857
+ if (Array.isArray(s.catchBlock)) visit(s.catchBlock as StatementIR[]);
1858
+ if (Array.isArray(s.finallyBlock)) visit(s.finallyBlock as StatementIR[]);
1859
+ if (Array.isArray(s.cases)) {
1860
+ for (const c of s.cases as Array<{ body?: StatementIR[] }>) {
1861
+ if (Array.isArray(c.body)) visit(c.body);
1862
+ }
1863
+ }
1864
+ }
1865
+ };
1866
+
1867
+ visit(program.topLevelStatements);
1868
+ for (const fn of program.functions) visit(fn.statements);
1869
+ for (const cls of program.classes) {
1870
+ for (const m of cls.methods) visit(m.statements);
1871
+ if (cls.constructor) visit(cls.constructor.statements);
1872
+ }
1873
+
1874
+ return diagnostics;
1875
+ }
1876
+
1877
+ /**
1878
+ * Detect whether the program uses console.* calls.
1879
+ */
1880
+ function detectConsoleUsage(program: ProgramIR): boolean {
1881
+ const consolePrefixes = ["console.log", "console.error", "console.warn", "console.info", "console.debug"];
1882
+ const checkStmts = (stmts: StatementIR[]): boolean => {
1883
+ for (const stmt of stmts) {
1884
+ if (stmt.kind === "call" && consolePrefixes.some(p => stmt.callee.startsWith(p))) return true;
1885
+ if ("body" in stmt && Array.isArray(stmt.body) && checkStmts(stmt.body)) return true;
1886
+ if ("thenBranch" in stmt && Array.isArray(stmt.thenBranch) && checkStmts(stmt.thenBranch)) return true;
1887
+ if ("elseBranch" in stmt && Array.isArray(stmt.elseBranch) && checkStmts(stmt.elseBranch)) return true;
1888
+ if ("cases" in stmt && Array.isArray((stmt as any).cases)) {
1889
+ for (const c of (stmt as any).cases) { if (checkStmts(c.body)) return true; }
1890
+ }
1891
+ if ("tryBlock" in stmt && Array.isArray(stmt.tryBlock) && checkStmts(stmt.tryBlock)) return true;
1892
+ if ("catchBlock" in stmt && Array.isArray(stmt.catchBlock) && checkStmts(stmt.catchBlock)) return true;
1893
+ }
1894
+ return false;
1895
+ };
1896
+ for (const fn of program.functions) { if (checkStmts(fn.statements)) return true; }
1897
+ if (checkStmts(program.topLevelStatements)) return true;
1898
+ for (const cls of program.classes) {
1899
+ for (const m of cls.methods) { if (checkStmts(m.statements)) return true; }
1900
+ if (cls.constructor && checkStmts(cls.constructor.statements)) return true;
1901
+ }
1902
+ return false;
1903
+ }
1904
+
1905
+ /**
1906
+ * Detect whether the program already calls Serial.begin (or similar .begin()).
1907
+ */
1908
+ function detectSerialBeginCall(program: ProgramIR): boolean {
1909
+ const checkStmt = (stmt: StatementIR): boolean => {
1910
+ if (stmt.kind === "call" && (stmt.callee === "Serial.begin" || stmt.callee.endsWith(".begin"))) return true;
1911
+ if ("body" in stmt && Array.isArray(stmt.body)) { for (const s of stmt.body) { if (checkStmt(s)) return true; } }
1912
+ if ("thenBranch" in stmt && Array.isArray(stmt.thenBranch)) { for (const s of stmt.thenBranch) { if (checkStmt(s)) return true; } }
1913
+ if ("elseBranch" in stmt && Array.isArray(stmt.elseBranch)) { for (const s of stmt.elseBranch) { if (checkStmt(s)) return true; } }
1914
+ if ("cases" in stmt && Array.isArray((stmt as any).cases)) {
1915
+ for (const c of (stmt as any).cases) { for (const s of c.body) { if (checkStmt(s)) return true; } }
1916
+ }
1917
+ if ("tryBlock" in stmt && Array.isArray(stmt.tryBlock)) { for (const s of stmt.tryBlock) { if (checkStmt(s)) return true; } }
1918
+ if ("catchBlock" in stmt && Array.isArray(stmt.catchBlock)) { for (const s of stmt.catchBlock) { if (checkStmt(s)) return true; } }
1919
+ return false;
1920
+ };
1921
+ for (const fn of program.functions) { for (const s of fn.statements) { if (checkStmt(s)) return true; } }
1922
+ for (const s of program.topLevelStatements) { if (checkStmt(s)) return true; }
1923
+ for (const cls of program.classes) {
1924
+ for (const m of cls.methods) { for (const s of m.statements) { if (checkStmt(s)) return true; } }
1925
+ if (cls.constructor) { for (const s of cls.constructor.statements) { if (checkStmt(s)) return true; } }
1926
+ }
1927
+ return false;
1928
+ }
1929
+
1930
+ /**
1931
+ * Scan IR to see if the program uses nullish coalescing (??), optional
1932
+ * chaining (?.), or undefined/null literals — all of which need the
1933
+ * CUTTLEFISH_UNDEFINED shims at runtime.
1934
+ */
1935
+ function programAnalysisUsesNullish(program: ProgramIR): boolean {
1936
+ // Recursively walk an expression tree for `??` operators and `undefined`/`null`
1937
+ // identifiers. The previous shallow check only inspected the top-level node of
1938
+ // each statement field, so `undefined` nested inside an object/struct literal
1939
+ // (e.g. `{ timeout: undefined }`) or an array element escaped detection — the
1940
+ // emitter still lowered it to CUTTLEFISH_UNDEFINED, but the nullish shim block
1941
+ // (which #defines that macro) was skipped, producing an undeclared-identifier
1942
+ // compile error. This walker covers every ExpressionIR nesting path.
1943
+ const exprUsesNullish = (e: any): boolean => {
1944
+ if (!e || typeof e !== "object") return false;
1945
+ switch (e.kind) {
1946
+ case "identifier":
1947
+ return e.value === "undefined" || e.value === "null";
1948
+ case "binary":
1949
+ return e.operator === "??" || exprUsesNullish(e.left) || exprUsesNullish(e.right);
1950
+ case "unary":
1951
+ return exprUsesNullish(e.operand);
1952
+ case "ternary":
1953
+ return exprUsesNullish(e.condition) || exprUsesNullish(e.whenTrue) || exprUsesNullish(e.whenFalse);
1954
+ case "paren":
1955
+ return exprUsesNullish(e.inner);
1956
+ case "await":
1957
+ return exprUsesNullish(e.value);
1958
+ case "template_string":
1959
+ return exprUsesNullish(e.expression);
1960
+ case "instanceof":
1961
+ return exprUsesNullish(e.object);
1962
+ case "tuple-access":
1963
+ return exprUsesNullish(e.object);
1964
+ case "property-access":
1965
+ return exprUsesNullish(e.object);
1966
+ case "element-access":
1967
+ return exprUsesNullish(e.object) || exprUsesNullish(e.index);
1968
+ case "array":
1969
+ return (e.elements as any[])?.some(exprUsesNullish) ?? false;
1970
+ case "string_concat":
1971
+ return (e.parts as any[])?.some(exprUsesNullish) ?? false;
1972
+ case "object":
1973
+ return (e.fields as any[])?.some((f) => exprUsesNullish(f?.value)) ?? false;
1974
+ case "spread_array":
1975
+ return exprUsesNullish(e.spreadExpr) || ((e.additionalElements as any[])?.some(exprUsesNullish) ?? false);
1976
+ case "method-call":
1977
+ return (e.args as any[])?.some(exprUsesNullish) ?? false;
1978
+ // Leaf nodes: number, string, boolean, raw, hal-expr — no nested nullish.
1979
+ // callback/lambda embed StatementIR[] (handled by the statement walker
1980
+ // below via body/params), not ExpressionIR, so stop here.
1981
+ default:
1982
+ return false;
1983
+ }
1984
+ };
1985
+
1986
+ const check = (s: StatementIR): boolean => {
1987
+ // Check every statement field that holds an ExpressionIR, recursing fully.
1988
+ for (const field of ["condition", "initializer", "value", "left", "right", "operand", "iterable", "subject", "expression", "delegate"] as const) {
1989
+ if (field in s) {
1990
+ const val = (s as any)[field];
1991
+ if (val && typeof val === "object" && "kind" in val) {
1992
+ if (exprUsesNullish(val)) return true;
1993
+ }
1994
+ }
1995
+ }
1996
+ for (const key of ["body", "thenBranch", "elseBranch", "tryBlock", "catchBlock"]) {
1997
+ if (key in s && Array.isArray((s as any)[key])) {
1998
+ for (const child of (s as any)[key]) { if (check(child)) return true; }
1999
+ }
2000
+ }
2001
+ return false;
2002
+ };
2003
+ for (const fn of program.functions) { for (const s of fn.statements) { if (check(s)) return true; } }
2004
+ for (const s of program.topLevelStatements) { if (check(s)) return true; }
2005
+ for (const cls of program.classes) {
2006
+ for (const m of cls.methods) { for (const s of m.statements) { if (check(s)) return true; } }
2007
+ }
2008
+ return false;
2009
+ }
2010
+
2011
+ /**
2012
+ * Scan IR to see if createPinGroup is called.
2013
+ */
2014
+ function detectPinGroupUsage(program: ProgramIR): boolean {
2015
+ const checkStmt = (stmt: StatementIR): boolean => {
2016
+ if (stmt.kind === "call" && stmt.callee === "createPinGroup") return true;
2017
+ // Also detect createPinGroup inside variable declarations
2018
+ // (e.g. const leds = createPinGroup([...]))
2019
+ if (stmt.kind === "var_decl" && stmt.initializer) {
2020
+ const init = stmt.initializer as any;
2021
+ if (init.kind === "method-call" && init.callee === "createPinGroup") return true;
2022
+ // Also check raw-call expressions (some IR paths store bare function calls differently)
2023
+ if (typeof init.callee === "string" && init.callee.includes("createPinGroup")) return true;
2024
+ }
2025
+ if ("body" in stmt && Array.isArray(stmt.body)) { for (const s of stmt.body) { if (checkStmt(s)) return true; } }
2026
+ if ("thenBranch" in stmt && Array.isArray(stmt.thenBranch)) { for (const s of stmt.thenBranch) { if (checkStmt(s)) return true; } }
2027
+ if ("elseBranch" in stmt && Array.isArray(stmt.elseBranch)) { for (const s of stmt.elseBranch) { if (checkStmt(s)) return true; } }
2028
+ if ("cases" in stmt && Array.isArray((stmt as any).cases)) {
2029
+ for (const c of (stmt as any).cases) { for (const s of c.body) { if (checkStmt(s)) return true; } }
2030
+ }
2031
+ if ("tryBlock" in stmt && Array.isArray(stmt.tryBlock)) { for (const s of stmt.tryBlock) { if (checkStmt(s)) return true; } }
2032
+ if ("catchBlock" in stmt && Array.isArray(stmt.catchBlock)) { for (const s of stmt.catchBlock) { if (checkStmt(s)) return true; } }
2033
+ return false;
2034
+ };
2035
+ for (const fn of program.functions) { for (const s of fn.statements) { if (checkStmt(s)) return true; } }
2036
+ for (const s of program.topLevelStatements) { if (checkStmt(s)) return true; }
2037
+ for (const cls of program.classes) {
2038
+ for (const m of cls.methods) { for (const s of m.statements) { if (checkStmt(s)) return true; } }
2039
+ if (cls.constructor) { for (const s of cls.constructor.statements) { if (checkStmt(s)) return true; } }
2040
+ }
2041
+ return false;
2042
+ }
2043
+ /**
2044
+ * Split a stream-chain expression ("a" << b << "c << d") into its parts,
2045
+ * without breaking on `<<` that appears inside a string literal. Each part is
2046
+ * returned trimmed. Used by transformConsoleCall to emit one Serial.print per
2047
+ * argument (HardwareSerial has no operator<<).
2048
+ */
2049
+ function splitStreamChain(renderedArgs: string): string[] {
2050
+ const parts: string[] = [];
2051
+ let depth = 0; // paren/bracket nesting
2052
+ let inString = false; // inside a "..." literal
2053
+ let escape = false; // previous char was backslash
2054
+ let start = 0;
2055
+ for (let i = 0; i < renderedArgs.length; i++) {
2056
+ const c = renderedArgs[i];
2057
+ if (inString) {
2058
+ if (escape) { escape = false; continue; }
2059
+ if (c === "\\") { escape = true; continue; }
2060
+ if (c === '"') inString = false;
2061
+ continue;
2062
+ }
2063
+ if (c === '"') { inString = true; continue; }
2064
+ if (c === "(" || c === "[") depth++;
2065
+ else if (c === ")" || c === "]") depth--;
2066
+ else if (c === "<" && depth === 0 && renderedArgs[i + 1] === "<") {
2067
+ parts.push(renderedArgs.slice(start, i).trim());
2068
+ i++; // skip second '<'
2069
+ start = i + 1;
2070
+ }
2071
+ }
2072
+ parts.push(renderedArgs.slice(start).trim());
2073
+ return parts.filter((p) => p.length > 0);
2074
+ }
2075
+
2076
+ /**
2077
+ * Wrap a bare C-string literal in F() so it lands in program memory (Flash)
2078
+ * instead of RAM — matches the single-arg console.log path. Non-literal parts
2079
+ * (numbers, identifiers, expressions) pass through unchanged.
2080
+ */
2081
+ function wrapArg(part: string): string {
2082
+ if (/^"[^"]*"$/.test(part)) return `F(${part})`;
2083
+ return part;
2084
+ }
2085
+
2086
+ /**
2087
+ * Map a TypeCAD bit-order value to the Arduino MSBFIRST/LSBFIRST macro.
2088
+ * Accepts `"msb"`/`"lsb"` (the HAL API), the Arduino macro names, or numeric
2089
+ * 1/0. Defensive quote-stripping + case-insensitive match mirrors the
2090
+ * analogReference mapping so any resolver path lands on the right constant.
2091
+ */
2092
+ function normalizeBitOrder(order: string | number): string {
2093
+ if (typeof order === "number") return order === 1 ? "MSBFIRST" : "LSBFIRST";
2094
+ const key = String(order).replace(/^["']|["']$/g, "").toLowerCase();
2095
+ if (key === "msb" || key === "msbfirst" || key === "1") return "MSBFIRST";
2096
+ if (key === "lsb" || key === "lsbfirst" || key === "0") return "LSBFIRST";
2097
+ return "LSBFIRST";
2098
+ }
2099
+
2100
+ /**
2101
+ * Map a TypeCAD WDT timeout to the AVR WDTO_* macro token. Accepts duration
2102
+ * presets ("250ms"), WDTO_* constant names ("WDTO_250MS", "250MS"), or a bare
2103
+ * number (passed through). Quote-stripped + case-insensitive, mirroring the
2104
+ * analogReference mapping. This must emit the unquoted macro so avr-gcc's
2105
+ * `wdt_enable(uint8_t)` receives the prescaler value, not a string pointer.
2106
+ */
2107
+ function normalizeWdtTimeout(timeout: string | number): string {
2108
+ if (typeof timeout === "number") return String(timeout);
2109
+ const raw = String(timeout).replace(/^["']|["']$/g, "");
2110
+ // Already a WDTO_* macro name — pass through unquoted.
2111
+ if (/^WDTO_\d+(MS|S)$/i.test(raw)) return raw.toUpperCase();
2112
+ const key = raw.toUpperCase();
2113
+ const wdtMap: Record<string, string> = {
2114
+ "15MS": "WDTO_15MS",
2115
+ "30MS": "WDTO_30MS",
2116
+ "60MS": "WDTO_60MS",
2117
+ "120MS": "WDTO_120MS",
2118
+ "250MS": "WDTO_250MS",
2119
+ "500MS": "WDTO_500MS",
2120
+ "1S": "WDTO_1S",
2121
+ "2S": "WDTO_2S",
2122
+ "4S": "WDTO_4S",
2123
+ "8S": "WDTO_8S",
2124
+ };
2125
+ return wdtMap[key] ?? raw;
2126
+ }