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