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