@typecad/cuttlefish 1.0.0-alpha.6 → 1.0.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/api/shared/display-adapter.d.ts +2 -1
  2. package/dist/api/shared/display-adapter.js +8 -1
  3. package/dist/api/shared/display-adapters/sdl.js +9 -2
  4. package/dist/api/shared/display-profile.d.ts +20 -3
  5. package/dist/api/shared/display-profile.js +21 -6
  6. package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
  7. package/dist/api/shared/framework-manifest-registry.js +25 -0
  8. package/dist/api/shared/framework-manifest.d.ts +462 -0
  9. package/dist/api/shared/framework-manifest.js +149 -0
  10. package/dist/api/shared/glcdfont.d.ts +12 -0
  11. package/dist/api/shared/glcdfont.js +124 -0
  12. package/dist/api/shared/graphics-strategy.d.ts +28 -0
  13. package/dist/api/shared/hal-op-ir.d.ts +418 -1
  14. package/dist/api/shared/hal-op-ir.js +95 -1
  15. package/dist/api/shared/index.d.ts +11 -1
  16. package/dist/api/shared/index.js +14 -0
  17. package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
  18. package/dist/api/shared/native-display-op-resolver.js +64 -0
  19. package/dist/api/shared/platform-strategy.d.ts +9 -0
  20. package/dist/api/shared/promise-runtime.js +78 -0
  21. package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
  22. package/dist/api/shared/validate-framework-manifest.js +417 -0
  23. package/dist/cli.js +109 -4
  24. package/dist/config-loader.js +20 -1
  25. package/dist/config-schema.d.ts +36 -36
  26. package/dist/create/board-spec.d.ts +4 -4
  27. package/dist/create/init-scaffold.d.ts +3 -0
  28. package/dist/create/init-scaffold.js +48 -0
  29. package/dist/create/init-templates.d.ts +2 -0
  30. package/dist/create/init-templates.js +28 -8
  31. package/dist/create/init-wizard.js +20 -1
  32. package/dist/emit/cpp-emitter.js +4 -3
  33. package/dist/emit/emitters/emitter-context.d.ts +5 -0
  34. package/dist/emit/emitters/function-emitter-impl.js +69 -59
  35. package/dist/emit/emitters/output-finalizer.d.ts +6 -0
  36. package/dist/emit/emitters/output-finalizer.js +21 -11
  37. package/dist/emit/emitters/setup.js +40 -0
  38. package/dist/emit/emitters/top-level-prep.js +2 -0
  39. package/dist/emit/emitters/ui-emitter.js +23 -8
  40. package/dist/emit/expression-renderer.js +10 -1
  41. package/dist/emit/snprintf-helpers.js +8 -0
  42. package/dist/emit/statement-renderer.js +13 -0
  43. package/dist/emit/utils/async-state-machine.js +185 -116
  44. package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
  45. package/dist/emit/utils/hal-op-cpp-type.js +40 -0
  46. package/dist/ir/build-ir.js +5 -1
  47. package/dist/ir/expression-to-ir.js +14 -0
  48. package/dist/ir/feature-registry.js +7 -25
  49. package/dist/ir/hal/hal-emitter.d.ts +5 -2
  50. package/dist/ir/hal/hal-emitter.js +40 -12
  51. package/dist/ir/hal/hal-parser.d.ts +6 -0
  52. package/dist/ir/hal/hal-parser.js +74 -0
  53. package/dist/ir/hal/hal-plugins.js +561 -0
  54. package/dist/ir/identifier-collector.js +18 -0
  55. package/dist/ir/network-validation.d.ts +4 -0
  56. package/dist/ir/network-validation.js +184 -0
  57. package/dist/ir/ownership-analysis.js +14 -1
  58. package/dist/ir/program-analysis.d.ts +23 -0
  59. package/dist/ir/program-analysis.js +81 -2
  60. package/dist/ir/timing-validation.d.ts +6 -1
  61. package/dist/ir/timing-validation.js +50 -12
  62. package/dist/ir/transformers/expressions.js +58 -0
  63. package/dist/ir/transformers/hal-emit-helpers.js +1 -1
  64. package/dist/ir/transformers/variables.js +41 -3
  65. package/dist/ir/validation-orchestrator.js +3 -1
  66. package/dist/libdef/c-to-decl.d.ts +27 -0
  67. package/dist/libdef/c-to-decl.js +397 -0
  68. package/dist/libdef/component-decls.d.ts +2 -0
  69. package/dist/libdef/component-decls.js +6 -0
  70. package/dist/libdef/component-discovery.d.ts +43 -0
  71. package/dist/libdef/component-discovery.js +83 -0
  72. package/dist/libdef/cpp-to-decl.d.ts +9 -0
  73. package/dist/libdef/cpp-to-decl.js +72 -0
  74. package/dist/libdef/idf-discovery.d.ts +7 -0
  75. package/dist/libdef/idf-discovery.js +59 -0
  76. package/dist/libdef/registry.js +5 -2
  77. package/dist/lint-cache.d.ts +59 -0
  78. package/dist/lint-cache.js +257 -0
  79. package/dist/orchestrator/graph-builder.js +6 -2
  80. package/dist/stores/display-profile-store.d.ts +1 -0
  81. package/dist/stores/display-profile-store.js +1 -0
  82. package/dist/testing.d.ts +3 -1
  83. package/dist/testing.js +3 -1
  84. package/dist/transpile.d.ts +3 -0
  85. package/dist/transpile.js +72 -27
  86. package/dist/ui-hook.d.ts +17 -1
  87. package/dist/utils/cli.js +27 -1
  88. package/dist/utils/fs.d.ts +13 -0
  89. package/dist/utils/fs.js +50 -0
  90. package/package.json +8 -4
@@ -435,5 +435,23 @@ function collectHALOpIdentifiers(op) {
435
435
  }
436
436
  // Other HAL ops have only numeric/literal fields — no identifier references
437
437
  }
438
+ // wifi.* / http.* ops carry resolved C++ expression texts in their string
439
+ // fields (e.g. wifi.connect ssid: `WIFI_SSID` — a top-level const, or a
440
+ // quoted literal). Scan every string field for identifiers so referenced
441
+ // globals survive tree-shaking; quoted literals are skipped.
442
+ if (op.operation.startsWith("wifi.") || op.operation.startsWith("http.")) {
443
+ for (const value of Object.values(op)) {
444
+ if (typeof value !== "string" || value === op.operation)
445
+ continue;
446
+ if (/^".*"$/.test(value.trim()))
447
+ continue;
448
+ const matches = value.match(/[A-Za-z_][A-Za-z0-9_]*/g);
449
+ if (matches) {
450
+ for (const match of matches) {
451
+ identifiers.add(match);
452
+ }
453
+ }
454
+ }
455
+ }
438
456
  return identifiers;
439
457
  }
@@ -0,0 +1,4 @@
1
+ import type { ProgramIR } from "../api/index.js";
2
+ import type { Diagnostic } from "../types.js";
3
+ import type { BoardConstants } from "./board-resolver.js";
4
+ export declare function validateNetworkUsage(program: ProgramIR, boardConstants?: BoardConstants): Diagnostic[];
@@ -0,0 +1,184 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Network (WiFi / HTTP) Validation
3
+ //
4
+ // Board-aware, compile-time checks for the WiFi/HTTP HAL:
5
+ //
6
+ // wifi-no-radio (error) wifi.*/http.* used on a board whose
7
+ // architecture has no WiFi radio (avr).
8
+ // http-without-wifi (warning) http.* used but the program never
9
+ // brings a WiFi link up (connect /
10
+ // connectSaved / connectAsync / AP).
11
+ // wifi-blocking-in-loop (warning) blocking WiFi.connect()/scan() or
12
+ // Http send inside loop() — stalls every
13
+ // iteration; use async/await instead.
14
+ // wifi-ap-password-short (error) WiFi.startAP() with a literal WPA2
15
+ // password shorter than 8 characters —
16
+ // esp_wifi rejects it at runtime.
17
+ // http-max-body-large (warning) Http maxBody() above 64 KB — the
18
+ // response buffer is heap-allocated.
19
+ //
20
+ // Follows the ADC-validation convention: checks that need board data return
21
+ // nothing when the data is missing (no MCU-name fallbacks).
22
+ // ---------------------------------------------------------------------------
23
+ import { walkProgramIR, walkNestedStatements } from "./utils/walk-ir.js";
24
+ /** Architectures with no WiFi radio. Driven by the `architecture` board
25
+ * constant (from the MCU package); absent data emits nothing. */
26
+ const NO_RADIO_ARCHITECTURES = new Set(["avr"]);
27
+ /** Ops that bring the WiFi link (STA or AP) up. */
28
+ const LINK_UP_OPS = new Set([
29
+ "wifi.connect",
30
+ "wifi.connect_start",
31
+ "wifi.connect_saved",
32
+ "wifi.ap_start",
33
+ ]);
34
+ /** Blocking waits that stall loop() for their full duration. */
35
+ const BLOCKING_LOOP_OPS = new Set([
36
+ "wifi.connect",
37
+ "wifi.connect_saved",
38
+ "wifi.wait_connected",
39
+ "wifi.wait_disconnected",
40
+ "wifi.scan",
41
+ "http.send",
42
+ ]);
43
+ /** Strip surrounding quotes from a resolved HAL string field; returns null
44
+ * when the field is not a literal (identifier/expression — unknowable). */
45
+ function literalString(value) {
46
+ if (typeof value !== "string")
47
+ return null;
48
+ const trimmed = value.trim();
49
+ const match = trimmed.match(/^"((?:[^"\\]|\\.)*)"$/);
50
+ return match ? match[1] : null;
51
+ }
52
+ function diagAt(stmt, partial) {
53
+ const s = stmt;
54
+ return {
55
+ ...partial,
56
+ line: s.sourceSpan?.startLine,
57
+ column: s.sourceSpan?.startColumn,
58
+ filePath: s.sourceSpan?.filePath,
59
+ source: "network-validation",
60
+ };
61
+ }
62
+ /** Pull every wifi./http. op out of a statement: hal-op statements, awaited
63
+ * __WIFI_WAIT__/__HTTP_WAIT__ marker calls (hal-expr arg), and hal-expr
64
+ * expressions in call args / initializers / conditions. */
65
+ function collectNetworkOps(stmt, out) {
66
+ const s = stmt;
67
+ const record = (op) => {
68
+ const operation = op?.operation;
69
+ if (typeof operation === "string" && (operation.startsWith("wifi.") || operation.startsWith("http."))) {
70
+ out.push({ op: op, stmt });
71
+ }
72
+ };
73
+ if (stmt.kind === "hal-op")
74
+ record(s.operation);
75
+ const visitExpr = (expr) => {
76
+ if (!expr || typeof expr !== "object")
77
+ return;
78
+ const e = expr;
79
+ if (e.kind === "hal-expr")
80
+ record(e.operation);
81
+ for (const key of ["args", "elements", "parts"]) {
82
+ if (Array.isArray(e[key]))
83
+ for (const child of e[key])
84
+ visitExpr(child);
85
+ }
86
+ for (const key of ["left", "right", "operand", "condition", "whenTrue", "whenFalse", "inner", "value", "expression"]) {
87
+ if (e[key] && typeof e[key] === "object")
88
+ visitExpr(e[key]);
89
+ }
90
+ };
91
+ for (const key of ["args", "value", "condition", "initializer", "expression"]) {
92
+ if (s[key]) {
93
+ if (Array.isArray(s[key]))
94
+ for (const child of s[key])
95
+ visitExpr(child);
96
+ else
97
+ visitExpr(s[key]);
98
+ }
99
+ }
100
+ }
101
+ export function validateNetworkUsage(program, boardConstants) {
102
+ const diagnostics = [];
103
+ const constants = boardConstants ?? program.boardConstants;
104
+ // Collect every wifi./http. op in the program.
105
+ const allOps = [];
106
+ walkProgramIR(program, (stmt) => collectNetworkOps(stmt, allOps));
107
+ if (allOps.length === 0)
108
+ return diagnostics;
109
+ const wifiOps = allOps.filter(({ op }) => op.operation.startsWith("wifi."));
110
+ const httpOps = allOps.filter(({ op }) => op.operation.startsWith("http."));
111
+ // ── wifi-no-radio ────────────────────────────────────────────────────────
112
+ const architecture = constants?.get("architecture");
113
+ const archName = typeof architecture === "string" ? architecture.replace(/^["']|["']$/g, "").toLowerCase() : null;
114
+ if (archName && NO_RADIO_ARCHITECTURES.has(archName)) {
115
+ const first = allOps[0];
116
+ diagnostics.push(diagAt(first.stmt, {
117
+ severity: "error",
118
+ code: "wifi-no-radio",
119
+ message: `WiFi/HTTP APIs are used but the target board's architecture ('${archName}') has no WiFi radio.`,
120
+ hint: "Target a WiFi-capable board (e.g. @typecad/board-esp32-devkit) or remove the WiFi/HTTP calls.",
121
+ }));
122
+ // No point piling on the remaining checks for a board that can't radio.
123
+ return diagnostics;
124
+ }
125
+ // ── http-without-wifi ────────────────────────────────────────────────────
126
+ if (httpOps.length > 0 && !wifiOps.some(({ op }) => LINK_UP_OPS.has(op.operation))) {
127
+ diagnostics.push(diagAt(httpOps[0].stmt, {
128
+ severity: "warning",
129
+ code: "http-without-wifi",
130
+ message: "HTTP requests are made but the program never brings the WiFi link up — every request will fail at runtime.",
131
+ hint: "Call WiFi.connect(ssid, password) (or WiFi.connectSaved() / WiFi.startAP()) before sending HTTP requests.",
132
+ }));
133
+ }
134
+ // ── wifi-blocking-in-loop ────────────────────────────────────────────────
135
+ // Blocking hal-op statements inside loop()'s body stall every iteration.
136
+ // Awaited forms are rewritten to __WIFI_WAIT__/__HTTP_WAIT__ marker calls
137
+ // (kind "call"), so only genuine blocking hal-ops are flagged here.
138
+ const loopFn = program.functions.find((fn) => fn.originalName === "loop");
139
+ if (loopFn?.statements) {
140
+ const visit = (stmt) => {
141
+ if (stmt.kind === "hal-op" && BLOCKING_LOOP_OPS.has(stmt.operation?.operation)) {
142
+ const opName = stmt.operation.operation;
143
+ diagnostics.push(diagAt(stmt, {
144
+ severity: "warning",
145
+ code: "wifi-blocking-in-loop",
146
+ message: `Blocking network operation '${opName}' inside loop() stalls every iteration until it completes (seconds for a connect/scan/request).`,
147
+ hint: "Move the call into an async function and `await` it — the transpiler lowers awaited WiFi/HTTP calls to non-blocking start + poll states.",
148
+ }));
149
+ }
150
+ walkNestedStatements(stmt, visit);
151
+ };
152
+ for (const stmt of loopFn.statements)
153
+ visit(stmt);
154
+ }
155
+ // ── wifi-ap-password-short ───────────────────────────────────────────────
156
+ for (const { op, stmt } of wifiOps) {
157
+ if (op.operation !== "wifi.ap_start")
158
+ continue;
159
+ const password = literalString(op.password);
160
+ if (password !== null && password.length > 0 && password.length < 8) {
161
+ diagnostics.push(diagAt(stmt, {
162
+ severity: "error",
163
+ code: "wifi-ap-password-short",
164
+ message: `WiFi.startAP() password is ${password.length} characters — WPA2 requires at least 8, and esp_wifi rejects shorter ones at runtime.`,
165
+ hint: "Use a password of 8+ characters, or omit the password entirely for an open access point.",
166
+ }));
167
+ }
168
+ }
169
+ // ── http-max-body-large ──────────────────────────────────────────────────
170
+ for (const { op, stmt } of httpOps) {
171
+ if (op.operation !== "http.set_max_body")
172
+ continue;
173
+ const bytes = Number(String(op.bytes).trim());
174
+ if (Number.isFinite(bytes) && bytes > 65536) {
175
+ diagnostics.push(diagAt(stmt, {
176
+ severity: "warning",
177
+ code: "http-max-body-large",
178
+ message: `Http maxBody(${bytes}) heap-allocates a ${Math.round(bytes / 1024)} KB response buffer — large buffers risk allocation failure under fragmentation.`,
179
+ hint: "Keep the response cap at or below 64 KB, or process large payloads in chunks server-side.",
180
+ }));
181
+ }
182
+ }
183
+ return diagnostics;
184
+ }
@@ -983,7 +983,20 @@ function validateConstSuggestions(program, diagnostics) {
983
983
  // keep the binding non-const so push_back/splice/etc. compile. The
984
984
  // callee is the lowered C++ name, e.g. "arr.push_back".
985
985
  if (stmt.kind === 'call' && typeof stmt.callee === 'string') {
986
- const callee = stmt.callee;
986
+ // Value-arg array methods (.push/.pop/.fill/...) are lowered in
987
+ // call-statement.ts to a fully-formed raw C++ expression wrapped as
988
+ // `__RAW_STMT__<receiver>.<method>(<args>)` (e.g. `__RAW_STMT__arr.push_back(1)`).
989
+ // Strip the wrapper — and the trailing `(args)` when present — before
990
+ // splitting, otherwise the dot parse yields a receiver of
991
+ // `__RAW_STMT__arr` and a method of `push_back(1)` and the demotion
992
+ // below never fires, leaving a non-compiling `const std::vector` +
993
+ // push_back. Mirrors the __RAW_STMT__ handling in program-analysis.ts.
994
+ const rawCallee = stmt.callee;
995
+ const unwrapped = rawCallee.startsWith('__RAW_STMT__')
996
+ ? rawCallee.slice('__RAW_STMT__'.length)
997
+ : rawCallee;
998
+ const parenIdx = unwrapped.indexOf('(');
999
+ const callee = parenIdx > 0 ? unwrapped.slice(0, parenIdx) : unwrapped;
987
1000
  const dot = callee.lastIndexOf('.');
988
1001
  if (dot > 0) {
989
1002
  const receiver = callee.slice(0, dot);
@@ -52,6 +52,29 @@ export interface ProgramAnalysisResult {
52
52
  * __tc_TimerRuntime::MAX_TIMERS to the observed count (floor 1) rather than
53
53
  * a blind constant, so a one-timer program links one slot, not eight. */
54
54
  timerCallCount: number;
55
+ /** ESP32 peripheral usage — framework-esp32 gates its IDF driver blocks and
56
+ * forced includes on these. Detected from HAL-op operation names, the same
57
+ * way usesUart/usesSPI/usesI2C are. Other frameworks have no CUTTLEFISH_*
58
+ * blocks with these marker names so the setup.ts filters are no-ops there. */
59
+ usesGPIO: boolean;
60
+ usesPWM: boolean;
61
+ usesRmt: boolean;
62
+ usesADC: boolean;
63
+ usesDAC: boolean;
64
+ usesPower: boolean;
65
+ usesWdt: boolean;
66
+ usesInterrupts: boolean;
67
+ usesPulse: boolean;
68
+ usesShift: boolean;
69
+ /** WiFi / HTTP / BLE usage — framework-esp32 gates the __tc_wifi/__tc_http/
70
+ * __tc_ble runtime shims and their esp_wifi/esp_http_client/nimble includes
71
+ * on these. Detected from wifi.* / http.* / ble.* HAL-op operation names. */
72
+ usesWifi: boolean;
73
+ usesHttp: boolean;
74
+ usesBle: boolean;
75
+ /** Preferences (NVS) usage — framework-esp32 gates the __tc_prefs runtime shim
76
+ * and its nvs_flash/nvs includes on this. Detected from preferences.* ops. */
77
+ usesPreferences: boolean;
55
78
  }
56
79
  /**
57
80
  * Analyze a program IR in a single pass to detect all features.
@@ -216,6 +216,23 @@ function analyzeExpression(expr, result, strategy) {
216
216
  analyzeExpression(expr.expression, result, strategy);
217
217
  }
218
218
  break;
219
+ case "hal-expr":
220
+ // Value-position HAL ops (e.g. `const ip = WiFi.localIP()` lowers to a
221
+ // hal-expr initializer) never pass through the statement-level hal-op
222
+ // detection above, so mirror the wifi./http. flag detection here. These
223
+ // gate the framework-esp32 __tc_wifi/__tc_http runtime shims.
224
+ if (expr.operation && typeof expr.operation.operation === "string") {
225
+ const opName = expr.operation.operation;
226
+ if (opName.startsWith("wifi."))
227
+ result.usesWifi = true;
228
+ if (opName.startsWith("http."))
229
+ result.usesHttp = true;
230
+ if (opName.startsWith("ble."))
231
+ result.usesBle = true;
232
+ if (opName.startsWith("preferences."))
233
+ result.usesPreferences = true;
234
+ }
235
+ break;
219
236
  }
220
237
  }
221
238
  /**
@@ -242,6 +259,11 @@ function analyzeStatement(statement, result, strategy) {
242
259
  if (statement.callee === "millis" || statement.callee === "delay") {
243
260
  result.usesMillis = true;
244
261
  }
262
+ // Awaited HAL wait markers become async state-machine poll states that
263
+ // arm deadlines via currentTimeMillis().
264
+ if (statement.callee === "__WIFI_WAIT__" || statement.callee === "__HTTP_WAIT__" || statement.callee === "__HAL_WAIT__") {
265
+ result.usesMillis = true;
266
+ }
245
267
  // Namespace-qualified polyfill entry points used as bare call statements
246
268
  // (e.g. `Timing.delay(5);`). The expression-level analyzer (case
247
269
  // "method-call") already checks these prefixes, but a statement-form call
@@ -264,7 +286,10 @@ function analyzeStatement(statement, result, strategy) {
264
286
  if (statement.callee === "tone" || statement.callee === "noTone") {
265
287
  result.usesTone = true;
266
288
  }
267
- if (statement.callee.startsWith("console.") || statement.callee.startsWith("_uart_")) {
289
+ // console.* is NOT UART: on ESP-IDF it lowers to printf, on Arduino to
290
+ // Serial via hasConsoleCalls. Only real UART HAL / Serial peripheral
291
+ // usage should gate the uart shim (avoids unused __tc_uart*_init).
292
+ if (statement.callee.startsWith("_uart_")) {
268
293
  result.usesUart = true;
269
294
  }
270
295
  if (statement.callee.startsWith("Serial.") || statement.callee === "Serial") {
@@ -424,6 +449,46 @@ function analyzeStatement(statement, result, strategy) {
424
449
  result.usesTone = true;
425
450
  if (opName.startsWith("uart."))
426
451
  result.usesUart = true;
452
+ // Display HAL ops (display.init from ui.mount, display.flush per frame)
453
+ // always imply GPIO usage (CS/DC/RST pins). Native SPI/I2C display
454
+ // adapters emit their own transport #includes (driver/spi_master.h,
455
+ // driver/i2c_master.h) — we don't force usesSPI/usesI2C here because
456
+ // that would pull headers even for non-display SPI/I2C code, and the
457
+ // display's transport type can't be determined from the op alone.
458
+ if (opName.startsWith("display.")) {
459
+ result.usesGPIO = true;
460
+ }
461
+ // ESP32 peripheral usage — framework-esp32 gates IDF driver blocks
462
+ // and forced includes on these. No-op for other frameworks (their
463
+ // shimLines emit no CUTTLEFISH_* blocks with these marker names).
464
+ if (opName.startsWith("gpio."))
465
+ result.usesGPIO = true;
466
+ if (opName.startsWith("pwm."))
467
+ result.usesPWM = true;
468
+ if (opName.startsWith("rmt."))
469
+ result.usesRmt = true;
470
+ if (opName.startsWith("adc."))
471
+ result.usesADC = true;
472
+ if (opName.startsWith("dac."))
473
+ result.usesDAC = true;
474
+ if (opName.startsWith("power."))
475
+ result.usesPower = true;
476
+ if (opName.startsWith("wdt."))
477
+ result.usesWdt = true;
478
+ if (opName.startsWith("interrupt."))
479
+ result.usesInterrupts = true;
480
+ if (opName.startsWith("pulse."))
481
+ result.usesPulse = true;
482
+ if (opName.startsWith("shift."))
483
+ result.usesShift = true;
484
+ if (opName.startsWith("wifi."))
485
+ result.usesWifi = true;
486
+ if (opName.startsWith("http."))
487
+ result.usesHttp = true;
488
+ if (opName.startsWith("ble."))
489
+ result.usesBle = true;
490
+ if (opName.startsWith("preferences."))
491
+ result.usesPreferences = true;
427
492
  // Timing HAL ops (timing.delay/millis/micros) carry a typed operation
428
493
  // name, not raw code, so the regex scans below miss them. Mirror the
429
494
  // raw-code timing detection here so usesMillis/usesTiming (and thus
@@ -467,7 +532,7 @@ function analyzeStatement(statement, result, strategy) {
467
532
  if (/\bEEPROM\b/.test(code) || /\beeprom_(read|write|update)_byte\b/.test(code)) {
468
533
  result.usesEEPROM = true;
469
534
  }
470
- if (/\b(Serial|console)\b/.test(code)) {
535
+ if (/\bSerial\b/.test(code)) {
471
536
  result.usesUart = true;
472
537
  }
473
538
  if (/\bSPI\b/.test(code)) {
@@ -535,6 +600,20 @@ export function analyzeProgram(program, strategy) {
535
600
  hasGenerators: false,
536
601
  usesStdMap: false,
537
602
  timerCallCount: 0,
603
+ usesGPIO: false,
604
+ usesPWM: false,
605
+ usesRmt: false,
606
+ usesADC: false,
607
+ usesDAC: false,
608
+ usesPower: false,
609
+ usesWdt: false,
610
+ usesInterrupts: false,
611
+ usesPulse: false,
612
+ usesShift: false,
613
+ usesWifi: false,
614
+ usesHttp: false,
615
+ usesBle: false,
616
+ usesPreferences: false,
538
617
  };
539
618
  // Analyze type aliases
540
619
  for (const typeAlias of program.typeAliases) {
@@ -1,7 +1,12 @@
1
1
  import type { ProgramIR } from '../api/index.js';
2
2
  import type { Diagnostic } from '../types.js';
3
+ import type { PlatformStrategy } from '../api/shared/index.js';
3
4
  /**
4
5
  * Detect blocking delay() calls inside loop()'s body. Emits a warning for
5
6
  * each, explaining the cooperative-async alternative.
7
+ *
8
+ * On RTOS targets (ESP-IDF), timing.delay lowers to vTaskDelay which yields
9
+ * the CPU — it does NOT freeze the async queue or UI. Only delayMicroseconds
10
+ * (busy-wait) triggers the warning.
6
11
  */
7
- export declare function validateBlockingDelayInLoop(program: ProgramIR): Diagnostic[];
12
+ export declare function validateBlockingDelayInLoop(program: ProgramIR, strategy?: PlatformStrategy): Diagnostic[];
@@ -12,20 +12,43 @@
12
12
  // g++ sees only an ordinary function. This validator flags blocking delays in
13
13
  // loop()'s direct body so the user can replace them with the cooperative
14
14
  // Async.sleep() / millis()-comparison pattern.
15
+ //
16
+ // IMPORTANT: on RTOS targets (ESP-IDF / FreeRTOS), delay() lowers to
17
+ // vTaskDelay which YIELDS the CPU — it does not freeze other tasks, UI
18
+ // rendering, or the async queue. Only delayMicroseconds (esp_rom_delay_us)
19
+ // is a true busy-wait. So for RTOS targets, only delay_microseconds triggers
20
+ // the warning; delay is safe.
15
21
  // ---------------------------------------------------------------------------
16
- /** Check a single statement for a blocking delay call or hal-op. */
17
- function isBlockingDelay(stmt) {
22
+ import { hasLoadedFramework, getLoadedFramework } from '../framework-registry.js';
23
+ /** Check a single statement for a blocking delay call or hal-op.
24
+ * On RTOS targets, timing.delay (vTaskDelay) is NOT blocking — only
25
+ * timing.delay_microseconds (esp_rom_delay_us) is. */
26
+ function isBlockingDelay(stmt, isRtos) {
18
27
  const s = stmt;
19
28
  // Direct call: delay(...) or delayMicroseconds(...)
20
29
  if (stmt.kind === 'call' && typeof s.callee === 'string') {
21
- if (s.callee === 'delay' || s.callee === 'delayMicroseconds')
22
- return true;
30
+ if (isRtos) {
31
+ // On RTOS targets, only delayMicroseconds is a busy-wait.
32
+ if (s.callee === 'delayMicroseconds')
33
+ return true;
34
+ }
35
+ else {
36
+ if (s.callee === 'delay' || s.callee === 'delayMicroseconds')
37
+ return true;
38
+ }
23
39
  }
24
40
  // Hal-op: after HAL resolution, delay() becomes timing.delay /
25
41
  // timing.delay_microseconds.
26
42
  if (stmt.kind === 'hal-op' && s.operation?.operation) {
27
- if (s.operation.operation === 'timing.delay' || s.operation.operation === 'timing.delay_microseconds') {
28
- return true;
43
+ if (isRtos) {
44
+ // On RTOS targets, only timing.delay_microseconds is blocking.
45
+ if (s.operation.operation === 'timing.delay_microseconds')
46
+ return true;
47
+ }
48
+ else {
49
+ if (s.operation.operation === 'timing.delay' || s.operation.operation === 'timing.delay_microseconds') {
50
+ return true;
51
+ }
29
52
  }
30
53
  }
31
54
  return false;
@@ -33,22 +56,37 @@ function isBlockingDelay(stmt) {
33
56
  /**
34
57
  * Detect blocking delay() calls inside loop()'s body. Emits a warning for
35
58
  * each, explaining the cooperative-async alternative.
59
+ *
60
+ * On RTOS targets (ESP-IDF), timing.delay lowers to vTaskDelay which yields
61
+ * the CPU — it does NOT freeze the async queue or UI. Only delayMicroseconds
62
+ * (busy-wait) triggers the warning.
36
63
  */
37
- export function validateBlockingDelayInLoop(program) {
64
+ export function validateBlockingDelayInLoop(program, strategy) {
38
65
  const diagnostics = [];
66
+ // Detect RTOS targets where delay() yields rather than blocks.
67
+ // framework-esp32 uses FreeRTOS; vTaskDelay is a yielding delay.
68
+ const isRtos = strategy?.isRtosTarget?.() === true
69
+ || (hasLoadedFramework() && getLoadedFramework().strategy.isRtosTarget?.() === true);
39
70
  const loopFn = program.functions.find(fn => fn.originalName === 'loop');
40
71
  if (!loopFn || !loopFn.statements)
41
72
  return diagnostics;
42
73
  const visit = (stmts) => {
43
74
  for (const stmt of stmts) {
44
- if (isBlockingDelay(stmt)) {
75
+ if (isBlockingDelay(stmt, isRtos)) {
45
76
  const s = stmt;
77
+ const message = isRtos
78
+ ? `Blocking delayMicroseconds() inside loop() is a busy-wait that wastes CPU cycles. ` +
79
+ `On RTOS targets (ESP-IDF), prefer timing.delay() (vTaskDelay) which yields the CPU to other tasks.`
80
+ : `Blocking delay() inside loop() freezes the async microtask queue and UI rendering ` +
81
+ `for the delay duration. This causes display tearing and makes the sketch unresponsive.`;
82
+ const hint = isRtos
83
+ ? `Replace delayMicroseconds with delay() if the timing permits, or accept the brief busy-wait if sub-millisecond precision is required.`
84
+ : `Use the cooperative pattern instead: track elapsed time with millis() comparisons, ` +
85
+ `or use Async.sleep(ms) / Async.yield() to let other tasks run between checks.`;
46
86
  diagnostics.push({
47
87
  severity: 'warning',
48
- message: `Blocking delay() inside loop() freezes the async microtask queue and UI rendering ` +
49
- `for the delay duration. This causes display tearing and makes the sketch unresponsive.`,
50
- hint: `Use the cooperative pattern instead: track elapsed time with millis() comparisons, ` +
51
- `or use Async.sleep(ms) / Async.yield() to let other tasks run between checks.`,
88
+ message,
89
+ hint,
52
90
  line: stmt.sourceSpan?.startLine,
53
91
  column: stmt.sourceSpan?.startColumn,
54
92
  filePath: stmt.sourceSpan?.filePath,
@@ -16,6 +16,17 @@ export function expressionStatementToIR(statement, fileName, sourceText, diagnos
16
16
  if (callStmt && callStmt.kind === "call") {
17
17
  return { ...callStmt, isAwaited: true };
18
18
  }
19
+ // Awaited network HAL ops (WiFi.connect / WiFi.untilConnected / Http.send
20
+ // / ...) resolve to hal-op statements, which would otherwise lower to the
21
+ // BLOCKING shim call even inside an async state machine. Rewrite them to
22
+ // an awaited __WIFI_WAIT__/__HTTP_WAIT__ marker call carrying the original
23
+ // op; the async state-machine generator turns it into a start + poll state
24
+ // pair, and the statement renderer falls back to the blocking form when
25
+ // the marker is rendered outside a state machine (top-level await, awaits
26
+ // nested in unsupported positions).
27
+ const netMarker = awaitedNetMarker(callStmt);
28
+ if (netMarker)
29
+ return netMarker;
19
30
  return callStmt;
20
31
  }
21
32
  // ── Register bit-field write ────────────────────────────────────────
@@ -404,3 +415,50 @@ export function expressionStatementToIR(statement, fileName, sourceText, diagnos
404
415
  }
405
416
  return undefined;
406
417
  }
418
+ /** HAL ops with a start/poll split available in the async state machine
419
+ * (see async-state-machine.ts netWaitInfo — keep the two in sync).
420
+ * timing.delay is included because `delay()` from @typecad/hal resolves to a
421
+ * hal-op, dropping the isAwaited flag the state machine keys on. */
422
+ const AWAITABLE_HAL_OPS = new Set([
423
+ "timing.delay",
424
+ "wifi.connect",
425
+ "wifi.wait_connected",
426
+ "wifi.wait_disconnected",
427
+ "wifi.scan",
428
+ "http.send",
429
+ "ble.until_connected",
430
+ ]);
431
+ /**
432
+ * Rewrite an awaited hal-op statement (or a block whose LAST statement is
433
+ * one — chained HAL calls resolve to blocks of hal-ops) into an awaited
434
+ * `__WIFI_WAIT__` / `__HTTP_WAIT__` / `__HAL_WAIT__` marker call carrying the
435
+ * original op as a hal-expr argument. Returns undefined when the statement is
436
+ * not an awaitable HAL op.
437
+ */
438
+ function awaitedNetMarker(stmt) {
439
+ if (!stmt)
440
+ return undefined;
441
+ if (stmt.kind === "hal-op" && AWAITABLE_HAL_OPS.has(stmt.operation.operation)) {
442
+ const opName = stmt.operation.operation;
443
+ const callee = opName.startsWith("http.") ? "__HTTP_WAIT__"
444
+ : opName.startsWith("wifi.") ? "__WIFI_WAIT__"
445
+ : opName.startsWith("ble.") ? "__BLE_WAIT__"
446
+ : "__HAL_WAIT__";
447
+ return {
448
+ kind: "call",
449
+ sourceSpan: stmt.sourceSpan,
450
+ leadingComments: stmt.leadingComments,
451
+ trailingComments: stmt.trailingComments,
452
+ callee,
453
+ args: [{ kind: "hal-expr", operation: stmt.operation }],
454
+ isAwaited: true,
455
+ };
456
+ }
457
+ if (stmt.kind === "block" && stmt.body.length > 0) {
458
+ const last = awaitedNetMarker(stmt.body[stmt.body.length - 1]);
459
+ if (last) {
460
+ return { ...stmt, body: [...stmt.body.slice(0, -1), last] };
461
+ }
462
+ }
463
+ return undefined;
464
+ }
@@ -27,7 +27,7 @@ export function collectChainedHALEmits(expr, sourceText, diagnostics, pointerVar
27
27
  }
28
28
  }
29
29
  // Continue recursion to collect deeper chain levels
30
- if (ts.isCallExpression(innerReceiver) && ts.isPropertyAccessExpression(innerReceiver)) {
30
+ if (ts.isCallExpression(innerReceiver) && ts.isPropertyAccessExpression(innerReceiver.expression)) {
31
31
  collectChainedHALEmits(innerReceiver, sourceText, diagnostics, pointerVars, emitLines, halOps);
32
32
  }
33
33
  }
@@ -7,7 +7,8 @@ import { getCurrentIrTypeScope, setScopeLocalType } from "../symbol-types.js";
7
7
  import { renderExprAsText } from "../render-expr.js";
8
8
  import { expressionToIR } from "../expression-to-ir.js";
9
9
  import { buildInlineForLoop } from "./array-methods.js";
10
- import { isKnownHALClass, getCtorIncludes, registerFloatVariable, resolveHALReceiver, isHALSingleton, } from "../hal-resolver.js";
10
+ import { isKnownHALClass, getCtorIncludes, getHALCtorFieldMap, registerFloatVariable, resolveHALReceiver, isHALSingleton, } from "../hal-resolver.js";
11
+ import { httpFactoryVerb, httpUrlArgText } from "../hal/hal-parser.js";
11
12
  import { resolveHALCallForVarInit } from "./hal-call-resolver.js";
12
13
  import { recordSignal } from "./ui-call-resolver.js";
13
14
  function replaceHalReadBufferPlaceholder(op, varName) {
@@ -408,9 +409,26 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
408
409
  }
409
410
  }
410
411
  }
412
+ // new HttpRequest(method, url) — positional ctor fields. The URL is
413
+ // stored as C++ expression text (quoted literal or identifier).
414
+ if (className === "HttpRequest" && ctorArgs && ctorArgs.length >= 2) {
415
+ const methodArg = ctorArgs[0];
416
+ if (ts.isStringLiteral(methodArg))
417
+ fieldValues.set("_method", methodArg.text.toUpperCase());
418
+ const urlText = httpUrlArgText(ctorArgs[1]);
419
+ if (urlText)
420
+ fieldValues.set("_url", urlText);
421
+ }
411
422
  if (ctorArgs) {
423
+ // Resolve a Pin-identifier ctor arg to its _pin number, for any HAL
424
+ // class whose first constructor field is _pin (Pin itself, plus
425
+ // pin-bearing wrappers like RmtChannel). Mirrors the Pin branch
426
+ // above but keyed off the class's registered ctor field map so new
427
+ // pin-keyed classes work without a per-class branch here.
428
+ const ctorFieldMap = getHALCtorFieldMap(className);
429
+ const firstFieldIsPin = ctorFieldMap && Array.from(ctorFieldMap.keys())[0] === "_pin";
412
430
  for (const arg of ctorArgs) {
413
- if (ts.isIdentifier(arg) && className === "Pin") {
431
+ if (ts.isIdentifier(arg) && (className === "Pin" || firstFieldIsPin)) {
414
432
  const existing = halInstances.get(arg.text);
415
433
  if (existing && existing.fieldValues.has("_pin")) {
416
434
  fieldValues.set("_pin", existing.fieldValues.get("_pin"));
@@ -516,6 +534,19 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
516
534
  }
517
535
  }
518
536
  }
537
+ // For Http factory calls (const req = Http.get(url)), record the
538
+ // HTTP verb + URL so req.send() can resolve this._method/this._url.
539
+ if (result.returnClassName === "HttpRequest") {
540
+ const verb = httpFactoryVerb(init.expression.name.text);
541
+ if (verb)
542
+ fieldValues.set("_method", verb);
543
+ const urlArg = init.arguments?.[0];
544
+ if (urlArg) {
545
+ const urlText = httpUrlArgText(urlArg);
546
+ if (urlText)
547
+ fieldValues.set("_url", urlText);
548
+ }
549
+ }
519
550
  halInstances.set(varName, {
520
551
  className: result.returnClassName,
521
552
  fieldValues,
@@ -573,7 +604,14 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
573
604
  // fill ops). Everything else is a scalar/value return captured
574
605
  // after the side-effect ops.
575
606
  const isTypedArray = result.returnValue.startsWith("__TYPED_ARRAY__:");
576
- if (!isTypedArray) {
607
+ // A factory returning a compile-time HAL instance (e.g.
608
+ // `const req = Http.get(url)` → returnValue `new HttpRequest(...)`)
609
+ // is fully tracked via halInstances — every later method call on
610
+ // the variable resolves at compile time. Emitting the raw
611
+ // `new HttpRequest(...)` would reference a class that doesn't
612
+ // exist in the C++ output.
613
+ const isHalInstanceReturn = !!result.returnClassName && /^new\s/.test(result.returnValue.trim());
614
+ if (!isTypedArray && !isHalInstanceReturn) {
577
615
  lowered.push({
578
616
  kind: "var_decl",
579
617
  sourceSpan: makeSourceSpan(declaration, fileName, sourceText),