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

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 (187) hide show
  1. package/dist/api/config.d.ts +32 -0
  2. package/dist/api/index.d.ts +1 -1
  3. package/dist/api/schema/types.d.ts +15 -0
  4. package/dist/api/shared/async-runtime-static.d.ts +12 -1
  5. package/dist/api/shared/async-runtime-static.js +69 -24
  6. package/dist/api/shared/async-symbol-detector.d.ts +11 -0
  7. package/dist/api/shared/async-symbol-detector.js +140 -0
  8. package/dist/api/shared/async-types.d.ts +24 -0
  9. package/dist/api/shared/coop-scheduler.d.ts +60 -0
  10. package/dist/api/shared/coop-scheduler.js +149 -0
  11. package/dist/api/shared/display-adapter.d.ts +4 -3
  12. package/dist/api/shared/display-adapter.js +15 -118
  13. package/dist/api/shared/display-adapters/sdl.js +38 -31
  14. package/dist/api/shared/display-profile.d.ts +47 -25
  15. package/dist/api/shared/display-profile.js +31 -164
  16. package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
  17. package/dist/api/shared/framework-manifest-registry.js +27 -0
  18. package/dist/api/shared/framework-manifest.d.ts +581 -0
  19. package/dist/api/shared/framework-manifest.js +182 -0
  20. package/dist/api/shared/glcdfont.d.ts +12 -0
  21. package/dist/api/shared/glcdfont.js +124 -0
  22. package/dist/api/shared/graphics-strategy.d.ts +37 -0
  23. package/dist/api/shared/hal-op-ir.d.ts +755 -1
  24. package/dist/api/shared/hal-op-ir.js +129 -1
  25. package/dist/api/shared/index.d.ts +20 -2
  26. package/dist/api/shared/index.js +25 -1
  27. package/dist/api/shared/ir-core.d.ts +4 -0
  28. package/dist/api/shared/ir-declarations.d.ts +6 -0
  29. package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
  30. package/dist/api/shared/native-display-op-resolver.js +64 -0
  31. package/dist/api/shared/platform-strategy.d.ts +113 -2
  32. package/dist/api/shared/polyfill-helper-registry.js +18 -1
  33. package/dist/api/shared/promise-runtime.d.ts +1 -1
  34. package/dist/api/shared/promise-runtime.js +95 -13
  35. package/dist/api/shared/toolchain-types.d.ts +17 -0
  36. package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
  37. package/dist/api/shared/validate-framework-manifest.js +494 -0
  38. package/dist/api/shared/worker-runtime-polyfill.d.ts +22 -0
  39. package/dist/api/shared/worker-runtime-polyfill.js +34 -0
  40. package/dist/api/shared/worker-runtime.d.ts +69 -0
  41. package/dist/api/shared/worker-runtime.js +163 -0
  42. package/dist/cli.js +186 -82
  43. package/dist/config-loader.d.ts +7 -2
  44. package/dist/config-loader.js +23 -3
  45. package/dist/config-schema.d.ts +112 -70
  46. package/dist/config-schema.js +14 -0
  47. package/dist/contract/board-generator.d.ts +17 -0
  48. package/dist/contract/board-generator.js +57 -0
  49. package/dist/contract/contract-parser.d.ts +217 -0
  50. package/dist/contract/contract-parser.js +224 -0
  51. package/dist/contract/index.d.ts +22 -0
  52. package/dist/contract/index.js +68 -0
  53. package/dist/create/board-codegen.js +4 -4
  54. package/dist/create/board-generators.js +4 -5
  55. package/dist/create/board-spec.d.ts +72 -75
  56. package/dist/create/board-spec.js +0 -1
  57. package/dist/create/init-scaffold.d.ts +9 -2
  58. package/dist/create/init-scaffold.js +0 -14
  59. package/dist/create/init-templates.d.ts +2 -0
  60. package/dist/create/init-templates.js +74 -8
  61. package/dist/create/init-wizard.js +31 -7
  62. package/dist/debug/preprocessor.js +178 -39
  63. package/dist/debug/types.d.ts +23 -0
  64. package/dist/diagnostics/mermaid-builder.d.ts +1 -1
  65. package/dist/diagnostics/mermaid-builder.js +34 -24
  66. package/dist/emit/compliance/arxml-writer.d.ts +11 -0
  67. package/dist/emit/compliance/arxml-writer.js +34 -0
  68. package/dist/emit/compliance/compliance-context.d.ts +57 -0
  69. package/dist/emit/compliance/compliance-context.js +113 -0
  70. package/dist/emit/compliance/deviation-ledger.d.ts +27 -0
  71. package/dist/emit/compliance/deviation-ledger.js +47 -0
  72. package/dist/emit/compliance/deviation-writer.d.ts +30 -0
  73. package/dist/emit/compliance/deviation-writer.js +37 -0
  74. package/dist/emit/compliance/index.d.ts +7 -0
  75. package/dist/emit/compliance/index.js +6 -0
  76. package/dist/emit/compliance/rule-engine.d.ts +13 -0
  77. package/dist/emit/compliance/rule-engine.js +101 -0
  78. package/dist/emit/compliance/rules.d.ts +16 -0
  79. package/dist/emit/compliance/rules.js +191 -0
  80. package/dist/emit/compliance/types.d.ts +59 -0
  81. package/dist/emit/compliance/types.js +8 -0
  82. package/dist/emit/cpp-emitter.js +4 -3
  83. package/dist/emit/emitters/class-emitter.js +6 -1
  84. package/dist/emit/emitters/emitter-context.d.ts +28 -1
  85. package/dist/emit/emitters/function-emitter-impl.js +153 -53
  86. package/dist/emit/emitters/line-appender.js +19 -0
  87. package/dist/emit/emitters/line-marker.d.ts +38 -0
  88. package/dist/emit/emitters/line-marker.js +39 -0
  89. package/dist/emit/emitters/output-finalizer.d.ts +6 -0
  90. package/dist/emit/emitters/output-finalizer.js +101 -12
  91. package/dist/emit/emitters/setup.d.ts +17 -0
  92. package/dist/emit/emitters/setup.js +257 -36
  93. package/dist/emit/emitters/top-level-prep.js +8 -0
  94. package/dist/emit/emitters/type-decl-emitter.js +28 -3
  95. package/dist/emit/emitters/ui-emitter.js +33 -9
  96. package/dist/emit/expression-renderer.d.ts +1 -1
  97. package/dist/emit/expression-renderer.js +91 -6
  98. package/dist/emit/route-hal-op.js +18 -5
  99. package/dist/emit/snprintf-helpers.js +15 -4
  100. package/dist/emit/statement-renderer.d.ts +10 -0
  101. package/dist/emit/statement-renderer.js +34 -5
  102. package/dist/emit/utils/async-state-machine.js +221 -125
  103. package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
  104. package/dist/emit/utils/hal-op-cpp-type.js +40 -0
  105. package/dist/framework-package.js +2 -0
  106. package/dist/framework-registry.d.ts +17 -0
  107. package/dist/ir/adc-range-validation.js +14 -1
  108. package/dist/ir/build-ir-state.d.ts +1 -0
  109. package/dist/ir/build-ir-state.js +14 -0
  110. package/dist/ir/build-ir.js +9 -5
  111. package/dist/ir/call-graph.js +16 -0
  112. package/dist/ir/expression-to-ir.js +164 -0
  113. package/dist/ir/feature-registry.js +7 -25
  114. package/dist/ir/function-builder.js +22 -0
  115. package/dist/ir/hal/hal-emitter.d.ts +5 -2
  116. package/dist/ir/hal/hal-emitter.js +50 -18
  117. package/dist/ir/hal/hal-parser.d.ts +6 -0
  118. package/dist/ir/hal/hal-parser.js +81 -0
  119. package/dist/ir/hal/hal-plugins.js +723 -1
  120. package/dist/ir/identifier-collector.js +35 -0
  121. package/dist/ir/interrupt-analysis.d.ts +5 -1
  122. package/dist/ir/interrupt-analysis.js +8 -16
  123. package/dist/ir/network-validation.d.ts +4 -0
  124. package/dist/ir/network-validation.js +184 -0
  125. package/dist/ir/ownership-analysis.js +20 -1
  126. package/dist/ir/pin-capability-validation.js +37 -0
  127. package/dist/ir/pin-mode-validation.d.ts +2 -2
  128. package/dist/ir/pin-mode-validation.js +49 -18
  129. package/dist/ir/program-analysis.d.ts +65 -0
  130. package/dist/ir/program-analysis.js +318 -2
  131. package/dist/ir/render-expr.js +11 -0
  132. package/dist/ir/timing-validation.d.ts +6 -1
  133. package/dist/ir/timing-validation.js +60 -13
  134. package/dist/ir/transformers/call-statement.js +105 -0
  135. package/dist/ir/transformers/expressions.js +62 -0
  136. package/dist/ir/transformers/hal-call-resolver.js +21 -0
  137. package/dist/ir/transformers/hal-emit-helpers.js +1 -1
  138. package/dist/ir/transformers/namespace-methods.js +17 -12
  139. package/dist/ir/transformers/ui-reactive.js +2 -2
  140. package/dist/ir/transformers/variables.js +92 -3
  141. package/dist/ir/type-resolution.js +18 -0
  142. package/dist/ir/ui-element-auto-wire.js +7 -5
  143. package/dist/ir/utils/map-statements.d.ts +4 -0
  144. package/dist/ir/utils/map-statements.js +79 -0
  145. package/dist/ir/validation-orchestrator.js +9 -2
  146. package/dist/ir/worker-analysis.d.ts +10 -0
  147. package/dist/ir/worker-analysis.js +261 -0
  148. package/dist/libdef/c-to-decl.d.ts +27 -0
  149. package/dist/libdef/c-to-decl.js +397 -0
  150. package/dist/libdef/component-decls.d.ts +2 -0
  151. package/dist/libdef/component-decls.js +6 -0
  152. package/dist/libdef/component-discovery.d.ts +43 -0
  153. package/dist/libdef/component-discovery.js +83 -0
  154. package/dist/libdef/cpp-to-decl.d.ts +9 -0
  155. package/dist/libdef/cpp-to-decl.js +72 -0
  156. package/dist/libdef/registry.js +5 -2
  157. package/dist/lint-cache.d.ts +59 -0
  158. package/dist/lint-cache.js +257 -0
  159. package/dist/orchestrator/graph-builder.js +14 -9
  160. package/dist/platform/coop-scheduler-runtime.d.ts +19 -0
  161. package/dist/platform/coop-scheduler-runtime.js +41 -0
  162. package/dist/platform/generic-strategy.d.ts +15 -3
  163. package/dist/platform/generic-strategy.js +49 -4
  164. package/dist/safety/safety-bridge.d.ts +11 -0
  165. package/dist/safety/safety-bridge.js +48 -0
  166. package/dist/safety/sidecar-bridge.d.ts +5 -0
  167. package/dist/safety/sidecar-bridge.js +22 -0
  168. package/dist/safety-hook.d.ts +79 -0
  169. package/dist/safety-hook.js +35 -0
  170. package/dist/testing.d.ts +7 -1
  171. package/dist/testing.js +8 -1
  172. package/dist/transpile.d.ts +3 -0
  173. package/dist/transpile.js +175 -41
  174. package/dist/types.d.ts +13 -2
  175. package/dist/ui-hook.d.ts +17 -3
  176. package/dist/utils/cli.js +78 -4
  177. package/dist/utils/fs.d.ts +13 -0
  178. package/dist/utils/fs.js +50 -0
  179. package/package.json +24 -5
  180. package/dist/api/shared/display-adapters/eink-mono.d.ts +0 -2
  181. package/dist/api/shared/display-adapters/eink-mono.js +0 -53
  182. package/dist/api/shared/display-adapters/ssd1309.d.ts +0 -2
  183. package/dist/api/shared/display-adapters/ssd1309.js +0 -136
  184. package/dist/api/shared/display-adapters/st7796.d.ts +0 -2
  185. package/dist/api/shared/display-adapters/st7796.js +0 -132
  186. package/dist/licenses.d.ts +0 -185
  187. package/dist/licenses.js +0 -963
@@ -38,6 +38,20 @@ function analyzeExpression(expr, result, strategy) {
38
38
  }
39
39
  }
40
40
  }
41
+ // Timer polyfill sizing — Timing.setInterval/Timing.setTimeout in
42
+ // expression position (e.g. `const id = Timing.setInterval(...)`) lower to
43
+ // a raw expression whose value contains `__tc_setInterval(...)`. The
44
+ // callee-based counter in the "call" case only fires for bare timer
45
+ // calls; without this the timer_methods polyfill is gated off and the
46
+ // link fails: "'__tc_setInterval' was not declared in this scope".
47
+ {
48
+ const intervals = expr.value.match(/__tc_setInterval\s*\(/g);
49
+ if (intervals)
50
+ result.timerCallCount += intervals.length;
51
+ const timeouts = expr.value.match(/__tc_setTimeout\s*\(/g);
52
+ if (timeouts)
53
+ result.timerCallCount += timeouts.length;
54
+ }
41
55
  if (/\bString\s*\(/.test(expr.value)) {
42
56
  result.usesStringConversion = true;
43
57
  }
@@ -63,6 +77,24 @@ function analyzeExpression(expr, result, strategy) {
63
77
  if (expr.value.includes('__tc_str_ptr')) {
64
78
  result.usesStrPtr = true;
65
79
  }
80
+ if (/std::chrono|steady_clock/.test(expr.value)) {
81
+ result.usesChrono = true;
82
+ }
83
+ if (/std::set\s*</.test(expr.value)) {
84
+ result.usesSet = true;
85
+ }
86
+ if (/std::(sort|find|transform|copy|fill|remove|count|reverse|accumulate)\s*\(/.test(expr.value)) {
87
+ result.usesAlgorithm = true;
88
+ }
89
+ if (/\b(printf|fprintf|sprintf|snprintf)\s*\(/.test(expr.value)) {
90
+ result.usesCstdio = true;
91
+ }
92
+ if (/\bdigitalRead\s*\(/.test(expr.value)) {
93
+ result.usesDigitalRead = true;
94
+ }
95
+ if (/cuttlefish_halt\s*\(/.test(expr.value)) {
96
+ result.usesHalt = true;
97
+ }
66
98
  break;
67
99
  case "method-call":
68
100
  if (strategy.isConsoleCall(expr.callee)) {
@@ -216,6 +248,108 @@ function analyzeExpression(expr, result, strategy) {
216
248
  analyzeExpression(expr.expression, result, strategy);
217
249
  }
218
250
  break;
251
+ case "hal-expr":
252
+ // Value-position HAL ops (e.g. `const v = D32.readAnalog()` lowers to an
253
+ // adc.read hal-expr initializer; `const ip = WiFi.localIP()` to a wifi
254
+ // hal-expr) never pass through the statement-level hal-op detection
255
+ // below, so mirror the FULL peripheral-flag detection here. Without this,
256
+ // a value-returning HAL op used in a var-init or expression context fails
257
+ // to set its analysis flag — the framework then drops the matching init
258
+ // block + forced includes (e.g. __tc_adc_read was emitted but the
259
+ // CUTTLEFISH_ADC init block and adc_oneshot.h include were not). Keep this
260
+ // list in sync with the hal-op statement case at the bottom of this file.
261
+ if (expr.operation && typeof expr.operation.operation === "string") {
262
+ const opName = expr.operation.operation;
263
+ if (opName.startsWith("display.")) {
264
+ result.usesGPIO = true;
265
+ result.usesDisplay = true;
266
+ }
267
+ if (opName.startsWith("gpio."))
268
+ result.usesGPIO = true;
269
+ if (opName === "gpio.read")
270
+ result.usesDigitalRead = true;
271
+ if (opName.startsWith("pwm."))
272
+ result.usesPWM = true;
273
+ if (opName.startsWith("rmt."))
274
+ result.usesRmt = true;
275
+ if (opName.startsWith("adc."))
276
+ result.usesADC = true;
277
+ if (opName.startsWith("dac."))
278
+ result.usesDAC = true;
279
+ if (opName.startsWith("power."))
280
+ result.usesPower = true;
281
+ if (opName.startsWith("wdt."))
282
+ result.usesWdt = true;
283
+ // Safety HAL ops (@typecad/safety) — map each op to the
284
+ // __tc_safety_* polyfill helper it triggers, so the mode-table and
285
+ // voter polyfills survive tree-shaking when actually used. Matches
286
+ // the resolveSafetyOp output naming. Cast through string because the
287
+ // HALOpIR operation union is closed and doesn't include safety.* ops
288
+ // (those are owned by the optional @typecad/safety package).
289
+ const safetyOpName = opName;
290
+ if (safetyOpName === "safety.record_pin_mode") {
291
+ result.usedPolyfillHelpers.add("__tc_safety_record_pin_mode");
292
+ }
293
+ if (safetyOpName === "safety.read_safe") {
294
+ result.usedPolyfillHelpers.add("__tc_safety_read_safe");
295
+ }
296
+ if (safetyOpName === "safety.write_verify") {
297
+ result.usedPolyfillHelpers.add("__tc_safety_write_verify");
298
+ }
299
+ if (opName.startsWith("interrupt."))
300
+ result.usesInterrupts = true;
301
+ if (opName.startsWith("pulse."))
302
+ result.usesPulse = true;
303
+ if (opName.startsWith("shift."))
304
+ result.usesShift = true;
305
+ if (opName.startsWith("tone."))
306
+ result.usesTone = true;
307
+ if (opName.startsWith("i2c."))
308
+ result.usesI2C = true;
309
+ if (opName.startsWith("spi."))
310
+ result.usesSPI = true;
311
+ if (opName.startsWith("uart."))
312
+ result.usesUart = true;
313
+ if (opName.startsWith("wifi.")) {
314
+ result.usesWifi = true;
315
+ if (opName === "wifi.connect" || opName === "wifi.connect_start" || opName === "wifi.disconnect") {
316
+ result.usesWifiConnect = true;
317
+ if (opName === "wifi.connect")
318
+ result.usesWifiConnectBlocking = true;
319
+ }
320
+ else if (opName.startsWith("wifi.scan"))
321
+ result.usesWifiScan = true;
322
+ else if (opName === "wifi.status" || opName === "wifi.local_ip" || opName === "wifi.rssi" || opName === "wifi.mac")
323
+ result.usesWifiQuery = true;
324
+ else if (opName === "wifi.set_hostname")
325
+ result.usesWifiConfig = true;
326
+ }
327
+ if (opName.startsWith("http."))
328
+ result.usesHttp = true;
329
+ if (opName.startsWith("ble."))
330
+ result.usesBle = true;
331
+ if (opName.startsWith("preferences."))
332
+ result.usesPreferences = true;
333
+ if (opName.startsWith("random."))
334
+ result.usesRandom = true;
335
+ if (opName.startsWith("fs."))
336
+ result.usesFS = true;
337
+ if (opName.startsWith("mdns."))
338
+ result.usesMdns = true;
339
+ if (opName.startsWith("mqtt."))
340
+ result.usesMqtt = true;
341
+ if (opName.startsWith("ota."))
342
+ result.usesOta = true;
343
+ if (opName.startsWith("temp."))
344
+ result.usesTemp = true;
345
+ if (opName.startsWith("hwtimer."))
346
+ result.usesHwtimer = true;
347
+ if (opName.startsWith("capacitive."))
348
+ result.usesCapacitive = true;
349
+ if (opName.startsWith("worker."))
350
+ result.usesWorker = true;
351
+ }
352
+ break;
219
353
  }
220
354
  }
221
355
  /**
@@ -242,6 +376,11 @@ function analyzeStatement(statement, result, strategy) {
242
376
  if (statement.callee === "millis" || statement.callee === "delay") {
243
377
  result.usesMillis = true;
244
378
  }
379
+ // Awaited HAL wait markers become async state-machine poll states that
380
+ // arm deadlines via currentTimeMillis().
381
+ if (statement.callee === "__WIFI_WAIT__" || statement.callee === "__HTTP_WAIT__" || statement.callee === "__HAL_WAIT__") {
382
+ result.usesMillis = true;
383
+ }
245
384
  // Namespace-qualified polyfill entry points used as bare call statements
246
385
  // (e.g. `Timing.delay(5);`). The expression-level analyzer (case
247
386
  // "method-call") already checks these prefixes, but a statement-form call
@@ -264,7 +403,10 @@ function analyzeStatement(statement, result, strategy) {
264
403
  if (statement.callee === "tone" || statement.callee === "noTone") {
265
404
  result.usesTone = true;
266
405
  }
267
- if (statement.callee.startsWith("console.") || statement.callee.startsWith("_uart_")) {
406
+ // console.* is NOT UART: on ESP-IDF it lowers to printf, on Arduino to
407
+ // Serial via hasConsoleCalls. Only real UART HAL / Serial peripheral
408
+ // usage should gate the uart shim (avoids unused __tc_uart*_init).
409
+ if (statement.callee.startsWith("_uart_")) {
268
410
  result.usesUart = true;
269
411
  }
270
412
  if (statement.callee.startsWith("Serial.") || statement.callee === "Serial") {
@@ -284,6 +426,18 @@ function analyzeStatement(statement, result, strategy) {
284
426
  result.usesMap = true;
285
427
  if (statement.callee === "constrain")
286
428
  result.usesConstrain = true;
429
+ // Statement-form setInterval/setTimeout mirror the expression-level
430
+ // timer-count (analyzeExpression). A top-level `setInterval(...)` call
431
+ // stays a `call` statement (never becomes a method-call expression — see
432
+ // the comment above re: Timing./Num./WDT.), so without this mirror the
433
+ // timer polyfill gate (timerCallCount) stays 0 and __tc_setInterval is
434
+ // emitted as a call but never defined (demo: "'__tc_setInterval' was not
435
+ // declared in this scope"). The arrow-callback hoister renames the callee
436
+ // to __tc_setInterval/__tc_setTimeout, so check both pre- and post-rename.
437
+ if (statement.callee === "setInterval" || statement.callee === "setTimeout"
438
+ || statement.callee === "__tc_setInterval" || statement.callee === "__tc_setTimeout") {
439
+ result.timerCallCount++;
440
+ }
287
441
  // The HAL resolver lowers WDT.*/Timing.* namespace calls to bare AVR
288
442
  // library functions (WDT.reset() → wdt_reset(), Timing.delay() → delay(),
289
443
  // Timing.millis() → millis()). When that happens the `WDT.`/`Timing.`
@@ -311,6 +465,27 @@ function analyzeStatement(statement, result, strategy) {
311
465
  }
312
466
  }
313
467
  }
468
+ // Timer polyfill sizing for __EMIT__ statements (callee "__EMIT__"):
469
+ // Timing.setInterval/Timing.setTimeout can lower to an __EMIT__ call whose
470
+ // single string arg is the raw C++ payload, e.g.
471
+ // `return __tc_setInterval(<cb>, 500)`. The callee-based timer counter
472
+ // above only fires for bare setInterval/__tc_setInterval calls, so without
473
+ // this the timer_methods polyfill is gated off and the link fails:
474
+ // "'__tc_setInterval' was not declared in this scope". Count call sites
475
+ // embedded in the raw payload. (The hal-op raw-code path has a parallel
476
+ // counter below.)
477
+ if (statement.callee === "__EMIT__") {
478
+ for (const arg of statement.args) {
479
+ if (arg && arg.kind === "string" && typeof arg.value === "string") {
480
+ const matches = arg.value.match(/__tc_setInterval\s*\(/g);
481
+ if (matches)
482
+ result.timerCallCount += matches.length;
483
+ const timeouts = arg.value.match(/__tc_setTimeout\s*\(/g);
484
+ if (timeouts)
485
+ result.timerCallCount += timeouts.length;
486
+ }
487
+ }
488
+ }
314
489
  for (const arg of statement.args) {
315
490
  analyzeExpression(arg, result, strategy);
316
491
  }
@@ -424,6 +599,95 @@ function analyzeStatement(statement, result, strategy) {
424
599
  result.usesTone = true;
425
600
  if (opName.startsWith("uart."))
426
601
  result.usesUart = true;
602
+ // Display HAL ops (display.init from ui.mount, display.flush per frame)
603
+ // always imply GPIO usage (CS/DC/RST pins). Native SPI/I2C display
604
+ // adapters emit their own transport #includes (driver/spi_master.h,
605
+ // driver/i2c_master.h) — we don't force usesSPI/usesI2C here because
606
+ // that would pull headers even for non-display SPI/I2C code, and the
607
+ // display's transport type can't be determined from the op alone.
608
+ if (opName.startsWith("display.")) {
609
+ result.usesGPIO = true;
610
+ result.usesDisplay = true;
611
+ }
612
+ // ESP32 peripheral usage — framework-esp32 gates IDF driver blocks
613
+ // and forced includes on these. No-op for other frameworks (their
614
+ // shimLines emit no CUTTLEFISH_* blocks with these marker names).
615
+ if (opName.startsWith("gpio."))
616
+ result.usesGPIO = true;
617
+ if (opName === "gpio.read")
618
+ result.usesDigitalRead = true;
619
+ if (opName.startsWith("pwm."))
620
+ result.usesPWM = true;
621
+ if (opName.startsWith("rmt."))
622
+ result.usesRmt = true;
623
+ if (opName.startsWith("adc."))
624
+ result.usesADC = true;
625
+ if (opName.startsWith("dac."))
626
+ result.usesDAC = true;
627
+ if (opName.startsWith("power."))
628
+ result.usesPower = true;
629
+ if (opName.startsWith("wdt."))
630
+ result.usesWdt = true;
631
+ // Safety HAL ops (@typecad/safety) — map each op to the
632
+ // __tc_safety_* polyfill helper it triggers, so the mode-table and
633
+ // voter polyfills survive tree-shaking when actually used. Matches
634
+ // the resolveSafetyOp output naming. Cast through string because the
635
+ // HALOpIR operation union is closed and doesn't include safety.* ops
636
+ // (those are owned by the optional @typecad/safety package).
637
+ const safetyOpName = opName;
638
+ if (safetyOpName === "safety.record_pin_mode") {
639
+ result.usedPolyfillHelpers.add("__tc_safety_record_pin_mode");
640
+ }
641
+ if (safetyOpName === "safety.read_safe") {
642
+ result.usedPolyfillHelpers.add("__tc_safety_read_safe");
643
+ }
644
+ if (safetyOpName === "safety.write_verify") {
645
+ result.usedPolyfillHelpers.add("__tc_safety_write_verify");
646
+ }
647
+ if (opName.startsWith("interrupt."))
648
+ result.usesInterrupts = true;
649
+ if (opName.startsWith("pulse."))
650
+ result.usesPulse = true;
651
+ if (opName.startsWith("shift."))
652
+ result.usesShift = true;
653
+ if (opName.startsWith("wifi.")) {
654
+ result.usesWifi = true;
655
+ if (opName === "wifi.connect" || opName === "wifi.connect_start" || opName === "wifi.disconnect") {
656
+ result.usesWifiConnect = true;
657
+ if (opName === "wifi.connect")
658
+ result.usesWifiConnectBlocking = true;
659
+ }
660
+ else if (opName.startsWith("wifi.scan"))
661
+ result.usesWifiScan = true;
662
+ else if (opName === "wifi.status" || opName === "wifi.local_ip" || opName === "wifi.rssi" || opName === "wifi.mac")
663
+ result.usesWifiQuery = true;
664
+ else if (opName === "wifi.set_hostname")
665
+ result.usesWifiConfig = true;
666
+ }
667
+ if (opName.startsWith("http."))
668
+ result.usesHttp = true;
669
+ if (opName.startsWith("ble."))
670
+ result.usesBle = true;
671
+ if (opName.startsWith("preferences."))
672
+ result.usesPreferences = true;
673
+ if (opName.startsWith("random."))
674
+ result.usesRandom = true;
675
+ if (opName.startsWith("fs."))
676
+ result.usesFS = true;
677
+ if (opName.startsWith("mdns."))
678
+ result.usesMdns = true;
679
+ if (opName.startsWith("mqtt."))
680
+ result.usesMqtt = true;
681
+ if (opName.startsWith("ota."))
682
+ result.usesOta = true;
683
+ if (opName.startsWith("temp."))
684
+ result.usesTemp = true;
685
+ if (opName.startsWith("hwtimer."))
686
+ result.usesHwtimer = true;
687
+ if (opName.startsWith("capacitive."))
688
+ result.usesCapacitive = true;
689
+ if (opName.startsWith("worker."))
690
+ result.usesWorker = true;
427
691
  // Timing HAL ops (timing.delay/millis/micros) carry a typed operation
428
692
  // name, not raw code, so the regex scans below miss them. Mirror the
429
693
  // raw-code timing detection here so usesMillis/usesTiming (and thus
@@ -461,13 +725,30 @@ function analyzeStatement(statement, result, strategy) {
461
725
  result.usesTiming = true;
462
726
  result.usesMillis = true;
463
727
  }
728
+ // Timer polyfill sizing — `Timing.setInterval(...)` / `Timing.setTimeout(...)`
729
+ // are resolved by the HAL method resolver, which reads TimingClass's
730
+ // rawCpp body and emits a raw hal-op whose code is e.g.
731
+ // `return __tc_setInterval(<cb>, 500)`. The callee-based counter below
732
+ // (statement.callee === "__tc_setInterval") never fires for these — the
733
+ // statement is a hal-op, not a bare timer call — so without counting
734
+ // here the timer_methods polyfill is gated off (timerCallCount == 0)
735
+ // and the link fails: "'__tc_setInterval' was not declared in this
736
+ // scope". Count each __tc_setInterval/__tc_setTimeout call site in the
737
+ // raw code. The optional `return ` prefix is emitted by value-returning
738
+ // HAL methods (statement context), so match with or without it.
739
+ const intervalSites = code.match(/__tc_setInterval\s*\(/g);
740
+ if (intervalSites)
741
+ result.timerCallCount += intervalSites.length;
742
+ const timeoutSites = code.match(/__tc_setTimeout\s*\(/g);
743
+ if (timeoutSites)
744
+ result.timerCallCount += timeoutSites.length;
464
745
  // Native AVR peripheral usage inside raw hal-op code (e.g. the
465
746
  // EEPROM namespace lowers to `EEPROM.write(...)` in a raw hal-op;
466
747
  // Serial/SPI/Wire may appear as lowered library calls too).
467
748
  if (/\bEEPROM\b/.test(code) || /\beeprom_(read|write|update)_byte\b/.test(code)) {
468
749
  result.usesEEPROM = true;
469
750
  }
470
- if (/\b(Serial|console)\b/.test(code)) {
751
+ if (/\bSerial\b/.test(code)) {
471
752
  result.usesUart = true;
472
753
  }
473
754
  if (/\bSPI\b/.test(code)) {
@@ -535,6 +816,41 @@ export function analyzeProgram(program, strategy) {
535
816
  hasGenerators: false,
536
817
  usesStdMap: false,
537
818
  timerCallCount: 0,
819
+ usesGPIO: false,
820
+ usesPWM: false,
821
+ usesRmt: false,
822
+ usesADC: false,
823
+ usesDAC: false,
824
+ usesPower: false,
825
+ usesWdt: false,
826
+ usesInterrupts: false,
827
+ usesPulse: false,
828
+ usesShift: false,
829
+ usesWifi: false,
830
+ usesWifiConnect: false,
831
+ usesWifiConnectBlocking: false,
832
+ usesWifiQuery: false,
833
+ usesWifiScan: false,
834
+ usesWifiConfig: false,
835
+ usesHttp: false,
836
+ usesBle: false,
837
+ usesPreferences: false,
838
+ usesRandom: false,
839
+ usesFS: false,
840
+ usesMdns: false,
841
+ usesMqtt: false,
842
+ usesOta: false,
843
+ usesTemp: false,
844
+ usesHwtimer: false,
845
+ usesCapacitive: false,
846
+ usesWorker: false,
847
+ usesChrono: false,
848
+ usesSet: false,
849
+ usesAlgorithm: false,
850
+ usesCstdio: false,
851
+ usesDigitalRead: false,
852
+ usesDisplay: false,
853
+ usesHalt: false,
538
854
  };
539
855
  // Analyze type aliases
540
856
  for (const typeAlias of program.typeAliases) {
@@ -85,6 +85,17 @@ export function renderExprAsText(expr) {
85
85
  case "lambda":
86
86
  return `/* __lambda__ */`;
87
87
  case "method-call": {
88
+ // Structured-receiver path: when the method-call IR carries a receiverExpr
89
+ // (set by the safe.read().ok().fail() chain builder), render the receiver
90
+ // recursively so nested lambda args are NOT flattened into the callee
91
+ // string as /* __lambda__ */ placeholders. The flat callee path below
92
+ // destroys lambdas; this preserves them.
93
+ const receiverExpr = expr.receiverExpr;
94
+ const methodName = expr.methodName;
95
+ if (receiverExpr !== undefined && methodName !== undefined) {
96
+ const argsText = expr.args.map(a => renderExprAsText(a)).join(", ");
97
+ return `${renderExprAsText(receiverExpr)}.${methodName}(${argsText})`;
98
+ }
88
99
  const argsText = expr.args.map(a => renderExprAsText(a)).join(", ");
89
100
  return `${expr.callee}(${argsText})`;
90
101
  }
@@ -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,52 @@
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
- // Direct call: delay(...) or delayMicroseconds(...)
28
+ // Derive the blocking delay call names from the loaded framework's
29
+ // ISR-unsafe table (delay, delayMicroseconds on Wiring-derived frameworks).
30
+ // Fall back to those two names when no framework is loaded.
31
+ const frameworkUnsafe = hasLoadedFramework()
32
+ ? getLoadedFramework().strategy.isrUnsafeOperations?.()
33
+ : undefined;
34
+ const delayNames = frameworkUnsafe
35
+ ? [...frameworkUnsafe.keys()].filter(k => k === 'delay' || k === 'delayMicroseconds')
36
+ : ['delay', 'delayMicroseconds'];
37
+ // Direct call: a blocking delay call by name.
20
38
  if (stmt.kind === 'call' && typeof s.callee === 'string') {
21
- if (s.callee === 'delay' || s.callee === 'delayMicroseconds')
22
- return true;
39
+ if (isRtos) {
40
+ // On RTOS targets, only delayMicroseconds is a busy-wait.
41
+ if (s.callee === 'delayMicroseconds')
42
+ return true;
43
+ }
44
+ else {
45
+ if (delayNames.includes(s.callee))
46
+ return true;
47
+ }
23
48
  }
24
49
  // Hal-op: after HAL resolution, delay() becomes timing.delay /
25
50
  // timing.delay_microseconds.
26
51
  if (stmt.kind === 'hal-op' && s.operation?.operation) {
27
- if (s.operation.operation === 'timing.delay' || s.operation.operation === 'timing.delay_microseconds') {
28
- return true;
52
+ if (isRtos) {
53
+ // On RTOS targets, only timing.delay_microseconds is blocking.
54
+ if (s.operation.operation === 'timing.delay_microseconds')
55
+ return true;
56
+ }
57
+ else {
58
+ if (s.operation.operation === 'timing.delay' || s.operation.operation === 'timing.delay_microseconds') {
59
+ return true;
60
+ }
29
61
  }
30
62
  }
31
63
  return false;
@@ -33,22 +65,37 @@ function isBlockingDelay(stmt) {
33
65
  /**
34
66
  * Detect blocking delay() calls inside loop()'s body. Emits a warning for
35
67
  * each, explaining the cooperative-async alternative.
68
+ *
69
+ * On RTOS targets (ESP-IDF), timing.delay lowers to vTaskDelay which yields
70
+ * the CPU — it does NOT freeze the async queue or UI. Only delayMicroseconds
71
+ * (busy-wait) triggers the warning.
36
72
  */
37
- export function validateBlockingDelayInLoop(program) {
73
+ export function validateBlockingDelayInLoop(program, strategy) {
38
74
  const diagnostics = [];
75
+ // Detect RTOS targets where delay() yields rather than blocks.
76
+ // framework-esp32 uses FreeRTOS; vTaskDelay is a yielding delay.
77
+ const isRtos = strategy?.isRtosTarget?.() === true
78
+ || (hasLoadedFramework() && getLoadedFramework().strategy.isRtosTarget?.() === true);
39
79
  const loopFn = program.functions.find(fn => fn.originalName === 'loop');
40
80
  if (!loopFn || !loopFn.statements)
41
81
  return diagnostics;
42
82
  const visit = (stmts) => {
43
83
  for (const stmt of stmts) {
44
- if (isBlockingDelay(stmt)) {
84
+ if (isBlockingDelay(stmt, isRtos)) {
45
85
  const s = stmt;
86
+ const message = isRtos
87
+ ? `Blocking delayMicroseconds() inside loop() is a busy-wait that wastes CPU cycles. ` +
88
+ `On RTOS targets (ESP-IDF), prefer timing.delay() (vTaskDelay) which yields the CPU to other tasks.`
89
+ : `Blocking delay() inside loop() freezes the async microtask queue and UI rendering ` +
90
+ `for the delay duration. This causes display tearing and makes the sketch unresponsive.`;
91
+ const hint = isRtos
92
+ ? `Replace delayMicroseconds with delay() if the timing permits, or accept the brief busy-wait if sub-millisecond precision is required.`
93
+ : `Use the cooperative pattern instead: track elapsed time with millis() comparisons, ` +
94
+ `or use Async.sleep(ms) / Async.yield() to let other tasks run between checks.`;
46
95
  diagnostics.push({
47
96
  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.`,
97
+ message,
98
+ hint,
52
99
  line: stmt.sourceSpan?.startLine,
53
100
  column: stmt.sourceSpan?.startColumn,
54
101
  filePath: stmt.sourceSpan?.filePath,
@@ -4,6 +4,7 @@ import { getCurrentIrTypeScope } from "../symbol-types.js";
4
4
  import { extractNodeComments, makeSourceSpan } from "../ast-node-utils.js";
5
5
  import { tryResolveHALMethod } from "./hal-call-resolver.js";
6
6
  import { tryResolveUICall, isSignalName, resolveUIModuleImport, recordPressBinding, uiPressBindings, resolveNodeIndex, resolveNodeTag, watchPinSpecs, recordWatchPin, recordClickHandler, clickHandlers, pushUnknownElementDiagnostic } from "./ui-call-resolver.js";
7
+ import { hasSafetyHook, requireSafetyHook } from "../../safety-hook.js";
7
8
  import { lowerCallbackStatements, resolveCallbackArg, getCanvasAmbientCtx, getCanvasAmbientDiagnostics, getCanvasAmbientSourceText, } from "./ui-callback-lowering.js";
8
9
  import { rewriteCanvasCall } from "./canvas-lowering.js";
9
10
  import { callbackContextLabel, unsupportedStatementHint } from "./callback-context-registry.js";
@@ -127,6 +128,64 @@ function lowerPreludeCallback(cbArg, fileName, sourceText, diagnostics, label, p
127
128
  sourceSpan,
128
129
  };
129
130
  }
131
+ /** Detect `safe.<method>(...)` calls in statement position and lower them to
132
+ * a hal-op statement via the safety hook's resolveSemanticCall. Mirrors the
133
+ * UI call resolver pattern. Returns null when:
134
+ * - the safety engine is not loaded (hasSafetyHook() === false), or
135
+ * - the call is not a `safe.<method>(...)` shape, or
136
+ * - the hook declines to lower it (returns undefined).
137
+ *
138
+ * Expression-position safe.* calls (e.g. `const r = safe.read(pin)`) are
139
+ * handled separately in the variable-declaration transformer. */
140
+ function tryResolveSafetyCallStatement(call, fileName, sourceText) {
141
+ if (!hasSafetyHook())
142
+ return null;
143
+ // Shape: safe.<method>(args...) — PropertyAccessExpression on identifier "safe".
144
+ if (!ts.isPropertyAccessExpression(call.expression))
145
+ return null;
146
+ if (!ts.isIdentifier(call.expression.expression))
147
+ return null;
148
+ if (call.expression.expression.text !== "safe")
149
+ return null;
150
+ const method = call.expression.name.text;
151
+ // Extract simple literal/identifier arg values; non-literal args are passed
152
+ // as their rendered text so the hook can decide. The hook consumes
153
+ // safe.read (pin), safe.write (pin, value), and safe.pinMode (pin, mode).
154
+ // For safe.write, the value may be a property access (r.value), arithmetic
155
+ // expression, or variable — rendered to its C++ text form.
156
+ const argValues = call.arguments.map((a) => {
157
+ if (ts.isNumericLiteral(a))
158
+ return Number(a.text);
159
+ if (ts.isStringLiteral(a))
160
+ return a.text;
161
+ if (a.kind === ts.SyntaxKind.TrueKeyword)
162
+ return true;
163
+ if (a.kind === ts.SyntaxKind.FalseKeyword)
164
+ return false;
165
+ if (ts.isIdentifier(a)) {
166
+ const name = a.text;
167
+ if (name === "INPUT")
168
+ return 0;
169
+ if (name === "OUTPUT")
170
+ return 1;
171
+ if (name === "INPUT_PULLUP")
172
+ return 2;
173
+ return name;
174
+ }
175
+ // Property access (e.g. r.value), arithmetic, or other expression:
176
+ // render to the C++ expression text so the op can emit it inline.
177
+ return a.getText();
178
+ });
179
+ const op = requireSafetyHook().resolveSemanticCall?.(`safe.${method}`, argValues);
180
+ if (!op)
181
+ return null;
182
+ return {
183
+ kind: "hal-op",
184
+ operation: op,
185
+ returns_value: false,
186
+ sourceSpan: makeSourceSpan(call, fileName, sourceText),
187
+ };
188
+ }
130
189
  export function callToStatement(statementNode, call, fileName, sourceText, diagnostics, pointerVars = new Map()) {
131
190
  const comments = extractNodeComments(statementNode, sourceText);
132
191
  // ── setInterval/setTimeout arrow-callback hoisting (BEFORE HAL) ────────
@@ -448,6 +507,52 @@ export function callToStatement(statementNode, call, fileName, sourceText, diagn
448
507
  const uiResolved = tryResolveUICall(call, fileName, sourceText, diagnostics);
449
508
  if (uiResolved)
450
509
  return uiResolved;
510
+ // ---- safe.* — @typecad/safety authoring calls (statement position) ---
511
+ // Handles statement-position safe.* calls like safe.pinMode(pin, mode).
512
+ // The expression-position form (const r = safe.read(pin)) is handled in
513
+ // the variable-declaration transformer. Both produce hal-op IR nodes that
514
+ // routeHALOp() later dispatches to the safety hook.
515
+ const safetyResolved = tryResolveSafetyCallStatement(call, fileName, sourceText);
516
+ if (safetyResolved)
517
+ return safetyResolved;
518
+ // ---- safe.read/safe.write chained with .ok/.fail/.fault/.always (statement) ----
519
+ // A statement like `safe.read(pin).ok(r => {...}).fail(r => {...})` is a
520
+ // method chain on a safe.* result. The terminal safe.* interceptor above
521
+ // returns null for it (the shape isn't `safe.<method>` — it's `.fail` on a
522
+ // chain). Delegate to expressionToIR, which has tryResolveSafetyChain to
523
+ // build the structured nested method-call IR that preserves lambda args,
524
+ // then render to text for a __RAW_STMT__ statement.
525
+ if (hasSafetyHook() && ts.isPropertyAccessExpression(call.expression)) {
526
+ let walk = call;
527
+ let isSafeChain = false;
528
+ while (ts.isCallExpression(walk) && ts.isPropertyAccessExpression(walk.expression)) {
529
+ walk = walk.expression.expression;
530
+ if (ts.isCallExpression(walk) &&
531
+ ts.isPropertyAccessExpression(walk.expression) &&
532
+ ts.isIdentifier(walk.expression.expression) &&
533
+ walk.expression.expression.text === "safe") {
534
+ isSafeChain = true;
535
+ break;
536
+ }
537
+ }
538
+ if (isSafeChain) {
539
+ // Build the structured chain IR via expressionToIR (the chain resolver
540
+ // produces nested method-call nodes with receiverExpr so lambdas survive).
541
+ // Carry it as the arg of a __EXPR_STMT__ call statement; the statement
542
+ // renderer renders it via the EMIT-TIME expression renderer (which has
543
+ // renderLambda for inline [&](){...} lambdas), NOT via the build-time
544
+ // renderExprAsText (which flattens lambdas to /* __lambda__ */).
545
+ const exprIR = expressionToIR(call, sourceText, diagnostics, pointerVars);
546
+ return {
547
+ kind: "call",
548
+ sourceSpan: makeSourceSpan(call, fileName, sourceText),
549
+ leadingComments: comments.leadingComments,
550
+ trailingComments: comments.trailingComments,
551
+ callee: "__EXPR_STMT__",
552
+ args: [exprIR],
553
+ };
554
+ }
555
+ }
451
556
  // Handle super() calls in constructors - transform to super_call IR for class emitter
452
557
  if (call.expression.kind === ts.SyntaxKind.SuperKeyword) {
453
558
  return {