@typecad/cuttlefish 1.0.0-alpha.7 → 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 (160) 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 +2 -2
  12. package/dist/api/shared/display-adapter.js +9 -119
  13. package/dist/api/shared/display-adapters/sdl.js +29 -29
  14. package/dist/api/shared/display-profile.d.ts +38 -33
  15. package/dist/api/shared/display-profile.js +22 -170
  16. package/dist/api/shared/framework-manifest-registry.d.ts +1 -1
  17. package/dist/api/shared/framework-manifest-registry.js +8 -6
  18. package/dist/api/shared/framework-manifest.d.ts +147 -28
  19. package/dist/api/shared/framework-manifest.js +35 -2
  20. package/dist/api/shared/graphics-strategy.d.ts +15 -6
  21. package/dist/api/shared/hal-op-ir.d.ts +340 -3
  22. package/dist/api/shared/hal-op-ir.js +34 -0
  23. package/dist/api/shared/index.d.ts +9 -1
  24. package/dist/api/shared/index.js +11 -1
  25. package/dist/api/shared/ir-core.d.ts +4 -0
  26. package/dist/api/shared/ir-declarations.d.ts +6 -0
  27. package/dist/api/shared/platform-strategy.d.ts +104 -2
  28. package/dist/api/shared/polyfill-helper-registry.js +18 -1
  29. package/dist/api/shared/promise-runtime.d.ts +1 -1
  30. package/dist/api/shared/promise-runtime.js +17 -13
  31. package/dist/api/shared/toolchain-types.d.ts +17 -0
  32. package/dist/api/shared/validate-framework-manifest.js +77 -0
  33. package/dist/api/shared/worker-runtime-polyfill.d.ts +22 -0
  34. package/dist/api/shared/worker-runtime-polyfill.js +34 -0
  35. package/dist/api/shared/worker-runtime.d.ts +69 -0
  36. package/dist/api/shared/worker-runtime.js +163 -0
  37. package/dist/cli.js +120 -121
  38. package/dist/config-loader.d.ts +7 -2
  39. package/dist/config-loader.js +20 -19
  40. package/dist/config-schema.d.ts +107 -65
  41. package/dist/config-schema.js +14 -0
  42. package/dist/contract/board-generator.d.ts +17 -0
  43. package/dist/contract/board-generator.js +57 -0
  44. package/dist/contract/contract-parser.d.ts +217 -0
  45. package/dist/contract/contract-parser.js +224 -0
  46. package/dist/contract/index.d.ts +22 -0
  47. package/dist/contract/index.js +68 -0
  48. package/dist/create/board-codegen.js +4 -4
  49. package/dist/create/board-generators.js +4 -5
  50. package/dist/create/board-spec.d.ts +74 -77
  51. package/dist/create/board-spec.js +0 -1
  52. package/dist/create/init-scaffold.d.ts +7 -3
  53. package/dist/create/init-scaffold.js +0 -62
  54. package/dist/create/init-templates.js +62 -16
  55. package/dist/create/init-wizard.js +32 -27
  56. package/dist/debug/preprocessor.js +178 -39
  57. package/dist/debug/types.d.ts +23 -0
  58. package/dist/diagnostics/mermaid-builder.d.ts +1 -1
  59. package/dist/diagnostics/mermaid-builder.js +34 -24
  60. package/dist/emit/compliance/arxml-writer.d.ts +11 -0
  61. package/dist/emit/compliance/arxml-writer.js +34 -0
  62. package/dist/emit/compliance/compliance-context.d.ts +57 -0
  63. package/dist/emit/compliance/compliance-context.js +113 -0
  64. package/dist/emit/compliance/deviation-ledger.d.ts +27 -0
  65. package/dist/emit/compliance/deviation-ledger.js +47 -0
  66. package/dist/emit/compliance/deviation-writer.d.ts +30 -0
  67. package/dist/emit/compliance/deviation-writer.js +37 -0
  68. package/dist/emit/compliance/index.d.ts +7 -0
  69. package/dist/emit/compliance/index.js +6 -0
  70. package/dist/emit/compliance/rule-engine.d.ts +13 -0
  71. package/dist/emit/compliance/rule-engine.js +101 -0
  72. package/dist/emit/compliance/rules.d.ts +16 -0
  73. package/dist/emit/compliance/rules.js +191 -0
  74. package/dist/emit/compliance/types.d.ts +59 -0
  75. package/dist/emit/compliance/types.js +8 -0
  76. package/dist/emit/emitters/class-emitter.js +6 -1
  77. package/dist/emit/emitters/emitter-context.d.ts +23 -1
  78. package/dist/emit/emitters/function-emitter-impl.js +96 -6
  79. package/dist/emit/emitters/line-appender.js +19 -0
  80. package/dist/emit/emitters/line-marker.d.ts +38 -0
  81. package/dist/emit/emitters/line-marker.js +39 -0
  82. package/dist/emit/emitters/output-finalizer.js +80 -1
  83. package/dist/emit/emitters/setup.d.ts +17 -0
  84. package/dist/emit/emitters/setup.js +217 -36
  85. package/dist/emit/emitters/top-level-prep.js +6 -0
  86. package/dist/emit/emitters/type-decl-emitter.js +28 -3
  87. package/dist/emit/emitters/ui-emitter.js +17 -8
  88. package/dist/emit/expression-renderer.d.ts +1 -1
  89. package/dist/emit/expression-renderer.js +81 -5
  90. package/dist/emit/route-hal-op.js +18 -5
  91. package/dist/emit/snprintf-helpers.js +7 -4
  92. package/dist/emit/statement-renderer.d.ts +10 -0
  93. package/dist/emit/statement-renderer.js +21 -5
  94. package/dist/emit/utils/async-state-machine.js +39 -12
  95. package/dist/framework-package.js +2 -0
  96. package/dist/framework-registry.d.ts +17 -0
  97. package/dist/ir/adc-range-validation.js +14 -1
  98. package/dist/ir/build-ir-state.d.ts +1 -0
  99. package/dist/ir/build-ir-state.js +14 -0
  100. package/dist/ir/build-ir.js +4 -4
  101. package/dist/ir/call-graph.js +16 -0
  102. package/dist/ir/expression-to-ir.js +150 -0
  103. package/dist/ir/function-builder.js +22 -0
  104. package/dist/ir/hal/hal-emitter.js +10 -6
  105. package/dist/ir/hal/hal-parser.js +16 -9
  106. package/dist/ir/hal/hal-plugins.js +164 -3
  107. package/dist/ir/identifier-collector.js +21 -4
  108. package/dist/ir/interrupt-analysis.d.ts +5 -1
  109. package/dist/ir/interrupt-analysis.js +8 -16
  110. package/dist/ir/ownership-analysis.js +6 -0
  111. package/dist/ir/pin-capability-validation.js +37 -0
  112. package/dist/ir/pin-mode-validation.d.ts +2 -2
  113. package/dist/ir/pin-mode-validation.js +49 -18
  114. package/dist/ir/program-analysis.d.ts +42 -0
  115. package/dist/ir/program-analysis.js +243 -6
  116. package/dist/ir/render-expr.js +11 -0
  117. package/dist/ir/timing-validation.js +11 -2
  118. package/dist/ir/transformers/call-statement.js +105 -0
  119. package/dist/ir/transformers/expressions.js +4 -0
  120. package/dist/ir/transformers/hal-call-resolver.js +21 -0
  121. package/dist/ir/transformers/namespace-methods.js +17 -12
  122. package/dist/ir/transformers/ui-reactive.js +2 -2
  123. package/dist/ir/transformers/variables.js +51 -0
  124. package/dist/ir/type-resolution.js +18 -0
  125. package/dist/ir/ui-element-auto-wire.js +7 -5
  126. package/dist/ir/utils/map-statements.d.ts +4 -0
  127. package/dist/ir/utils/map-statements.js +79 -0
  128. package/dist/ir/validation-orchestrator.js +6 -1
  129. package/dist/ir/worker-analysis.d.ts +10 -0
  130. package/dist/ir/worker-analysis.js +261 -0
  131. package/dist/orchestrator/graph-builder.js +8 -7
  132. package/dist/platform/coop-scheduler-runtime.d.ts +19 -0
  133. package/dist/platform/coop-scheduler-runtime.js +41 -0
  134. package/dist/platform/generic-strategy.d.ts +15 -3
  135. package/dist/platform/generic-strategy.js +49 -4
  136. package/dist/safety/safety-bridge.d.ts +11 -0
  137. package/dist/safety/safety-bridge.js +48 -0
  138. package/dist/safety/sidecar-bridge.d.ts +5 -0
  139. package/dist/safety/sidecar-bridge.js +22 -0
  140. package/dist/safety-hook.d.ts +79 -0
  141. package/dist/safety-hook.js +35 -0
  142. package/dist/stores/display-profile-store.d.ts +0 -1
  143. package/dist/stores/display-profile-store.js +0 -1
  144. package/dist/testing.d.ts +4 -0
  145. package/dist/testing.js +5 -0
  146. package/dist/transpile.js +124 -35
  147. package/dist/types.d.ts +13 -2
  148. package/dist/ui-hook.d.ts +0 -2
  149. package/dist/utils/cli.js +51 -3
  150. package/package.json +20 -5
  151. package/dist/api/shared/display-adapters/eink-mono.d.ts +0 -2
  152. package/dist/api/shared/display-adapters/eink-mono.js +0 -53
  153. package/dist/api/shared/display-adapters/ssd1309.d.ts +0 -2
  154. package/dist/api/shared/display-adapters/ssd1309.js +0 -136
  155. package/dist/api/shared/display-adapters/st7796.d.ts +0 -2
  156. package/dist/api/shared/display-adapters/st7796.js +0 -132
  157. package/dist/libdef/idf-discovery.d.ts +0 -7
  158. package/dist/libdef/idf-discovery.js +0 -59
  159. package/dist/licenses.d.ts +0 -185
  160. 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)) {
@@ -217,20 +249,105 @@ function analyzeExpression(expr, result, strategy) {
217
249
  }
218
250
  break;
219
251
  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.
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.
224
261
  if (expr.operation && typeof expr.operation.operation === "string") {
225
262
  const opName = expr.operation.operation;
226
- if (opName.startsWith("wifi."))
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.")) {
227
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
+ }
228
327
  if (opName.startsWith("http."))
229
328
  result.usesHttp = true;
230
329
  if (opName.startsWith("ble."))
231
330
  result.usesBle = true;
232
331
  if (opName.startsWith("preferences."))
233
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;
234
351
  }
235
352
  break;
236
353
  }
@@ -309,6 +426,18 @@ function analyzeStatement(statement, result, strategy) {
309
426
  result.usesMap = true;
310
427
  if (statement.callee === "constrain")
311
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
+ }
312
441
  // The HAL resolver lowers WDT.*/Timing.* namespace calls to bare AVR
313
442
  // library functions (WDT.reset() → wdt_reset(), Timing.delay() → delay(),
314
443
  // Timing.millis() → millis()). When that happens the `WDT.`/`Timing.`
@@ -336,6 +465,27 @@ function analyzeStatement(statement, result, strategy) {
336
465
  }
337
466
  }
338
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
+ }
339
489
  for (const arg of statement.args) {
340
490
  analyzeExpression(arg, result, strategy);
341
491
  }
@@ -457,12 +607,15 @@ function analyzeStatement(statement, result, strategy) {
457
607
  // display's transport type can't be determined from the op alone.
458
608
  if (opName.startsWith("display.")) {
459
609
  result.usesGPIO = true;
610
+ result.usesDisplay = true;
460
611
  }
461
612
  // ESP32 peripheral usage — framework-esp32 gates IDF driver blocks
462
613
  // and forced includes on these. No-op for other frameworks (their
463
614
  // shimLines emit no CUTTLEFISH_* blocks with these marker names).
464
615
  if (opName.startsWith("gpio."))
465
616
  result.usesGPIO = true;
617
+ if (opName === "gpio.read")
618
+ result.usesDigitalRead = true;
466
619
  if (opName.startsWith("pwm."))
467
620
  result.usesPWM = true;
468
621
  if (opName.startsWith("rmt."))
@@ -475,20 +628,66 @@ function analyzeStatement(statement, result, strategy) {
475
628
  result.usesPower = true;
476
629
  if (opName.startsWith("wdt."))
477
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
+ }
478
647
  if (opName.startsWith("interrupt."))
479
648
  result.usesInterrupts = true;
480
649
  if (opName.startsWith("pulse."))
481
650
  result.usesPulse = true;
482
651
  if (opName.startsWith("shift."))
483
652
  result.usesShift = true;
484
- if (opName.startsWith("wifi."))
653
+ if (opName.startsWith("wifi.")) {
485
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
+ }
486
667
  if (opName.startsWith("http."))
487
668
  result.usesHttp = true;
488
669
  if (opName.startsWith("ble."))
489
670
  result.usesBle = true;
490
671
  if (opName.startsWith("preferences."))
491
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;
492
691
  // Timing HAL ops (timing.delay/millis/micros) carry a typed operation
493
692
  // name, not raw code, so the regex scans below miss them. Mirror the
494
693
  // raw-code timing detection here so usesMillis/usesTiming (and thus
@@ -526,6 +725,23 @@ function analyzeStatement(statement, result, strategy) {
526
725
  result.usesTiming = true;
527
726
  result.usesMillis = true;
528
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;
529
745
  // Native AVR peripheral usage inside raw hal-op code (e.g. the
530
746
  // EEPROM namespace lowers to `EEPROM.write(...)` in a raw hal-op;
531
747
  // Serial/SPI/Wire may appear as lowered library calls too).
@@ -611,9 +827,30 @@ export function analyzeProgram(program, strategy) {
611
827
  usesPulse: false,
612
828
  usesShift: false,
613
829
  usesWifi: false,
830
+ usesWifiConnect: false,
831
+ usesWifiConnectBlocking: false,
832
+ usesWifiQuery: false,
833
+ usesWifiScan: false,
834
+ usesWifiConfig: false,
614
835
  usesHttp: false,
615
836
  usesBle: false,
616
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,
617
854
  };
618
855
  // Analyze type aliases
619
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
  }
@@ -25,7 +25,16 @@ import { hasLoadedFramework, getLoadedFramework } from '../framework-registry.js
25
25
  * timing.delay_microseconds (esp_rom_delay_us) is. */
26
26
  function isBlockingDelay(stmt, isRtos) {
27
27
  const s = stmt;
28
- // 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.
29
38
  if (stmt.kind === 'call' && typeof s.callee === 'string') {
30
39
  if (isRtos) {
31
40
  // On RTOS targets, only delayMicroseconds is a busy-wait.
@@ -33,7 +42,7 @@ function isBlockingDelay(stmt, isRtos) {
33
42
  return true;
34
43
  }
35
44
  else {
36
- if (s.callee === 'delay' || s.callee === 'delayMicroseconds')
45
+ if (delayNames.includes(s.callee))
37
46
  return true;
38
47
  }
39
48
  }
@@ -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 {
@@ -427,6 +427,10 @@ const AWAITABLE_HAL_OPS = new Set([
427
427
  "wifi.scan",
428
428
  "http.send",
429
429
  "ble.until_connected",
430
+ // Worker offload: `await worker.submit(...)` rewrites to submit + poll
431
+ // worker.done — mirrors `await http.send(...)`. The start op is worker.submit
432
+ // (emitted by the previous state segment); the poll predicate is worker.done.
433
+ "worker.submit",
430
434
  ]);
431
435
  /**
432
436
  * Rewrite an awaited hal-op statement (or a block whose LAST statement is
@@ -270,5 +270,26 @@ export function tryResolveHALExpression(call, sourceText, diagnostics, pointerVa
270
270
  }
271
271
  }
272
272
  }
273
+ // Namespace method fallbacks (Pulse, Shift, Random) in expression context.
274
+ // Value-returning namespace methods (e.g. Random.upTo/between/int) lower to
275
+ // HAL ops; surface the last op as a hal-expr so the strategy lowers it to the
276
+ // platform PRNG (Arduino random(), ESP-IDF esp_random()). Without this, an
277
+ // inline `if (Random.upTo(5) > 2)` would fall through to a 0 placeholder.
278
+ if (ts.isIdentifier(receiver)) {
279
+ const ns = receiver.text;
280
+ const nsResult = resolveNamespaceMethodCall(ns, method, argIRs);
281
+ if (nsResult) {
282
+ if (nsResult.halOps.length > 0) {
283
+ const lastOp = nsResult.halOps[nsResult.halOps.length - 1];
284
+ return {
285
+ ir: { kind: "hal-expr", operation: lastOp },
286
+ sideEffects: nsResult.emitLines,
287
+ };
288
+ }
289
+ if (nsResult.returnValue && nsResult.returnValue !== "__hal_op_return__") {
290
+ return { ir: { kind: "raw", value: nsResult.returnValue }, sideEffects: nsResult.emitLines };
291
+ }
292
+ }
293
+ }
273
294
  return null;
274
295
  }
@@ -81,29 +81,34 @@ export function resolveNamespaceMethodCall(ns, method, argIRs) {
81
81
  }
82
82
  }
83
83
  if (ns === "Random") {
84
+ // Random.* lowers to the random.* HAL op family, which frameworks lower to
85
+ // their platform PRNG (Arduino random()/randomSeed(), ESP-IDF esp_random()).
86
+ // Previously these emitted bare `random()`/`randomSeed()` calls, which only
87
+ // resolve to symbols on Arduino-core frameworks — on ESP-IDF they were
88
+ // undefined and failed at C++ link time.
84
89
  if (method === "seed") {
85
- return { emitLines: [`randomSeed(${argText(0)});`], halOps: [] };
90
+ return { emitLines: [], halOps: [{ operation: "random.seed", seed: argText(0) }] };
86
91
  }
87
92
  if (method === "number") {
88
93
  const min = argText(0);
89
94
  const max = argText(1);
90
- return {
91
- emitLines: [],
92
- halOps: [],
93
- returnValue: max ? `random(${min}, ${max})` : `random(${min})`,
94
- };
95
+ if (max) {
96
+ return { emitLines: [], halOps: [{ operation: "random.range", min, max }], returnValue: "__hal_op_return__" };
97
+ }
98
+ // Single-arg form: Random.number(max) [0, max-1]
99
+ return { emitLines: [], halOps: [{ operation: "random.range", min: "0", max: min }], returnValue: "__hal_op_return__" };
95
100
  }
96
- // Current class API: Random.upTo(max) → random(max)
101
+ // Random.upTo(max) → [0, max-1]
97
102
  if (method === "upTo") {
98
- return { emitLines: [], halOps: [], returnValue: `random(${argText(0)})` };
103
+ return { emitLines: [], halOps: [{ operation: "random.range", min: "0", max: argText(0) }], returnValue: "__hal_op_return__" };
99
104
  }
100
- // Random.between(min, max) → random(min, max)
105
+ // Random.between(min, max) → [min, max-1]
101
106
  if (method === "between") {
102
- return { emitLines: [], halOps: [], returnValue: `random(${argText(0)}, ${argText(1)})` };
107
+ return { emitLines: [], halOps: [{ operation: "random.range", min: argText(0), max: argText(1) }], returnValue: "__hal_op_return__" };
103
108
  }
104
- // Random.int() → random(2147483647)
109
+ // Random.int() → non-negative 31-bit integer
105
110
  if (method === "int") {
106
- return { emitLines: [], halOps: [], returnValue: `random(2147483647)` };
111
+ return { emitLines: [], halOps: [{ operation: "random.int" }], returnValue: "__hal_op_return__" };
107
112
  }
108
113
  }
109
114
  return null;
@@ -30,9 +30,9 @@ function propEnum(property) {
30
30
  export function emitBindingEntry(spec) {
31
31
  // Text bindings wire to textFn (void fill-style); color bindings wire to fn.
32
32
  if (spec.property === "text") {
33
- return ` { .node=${spec.nodeIndex}, .prop=${propEnum(spec.property)}, .fn=nullptr, .textFn=${spec.fnName} },`;
33
+ return ` { .node=${spec.nodeIndex}, .prop=${propEnum(spec.property)}, .fn=nullptr, .textFn=${spec.fnName}, .lastValue=0, .initialized=0 },`;
34
34
  }
35
- return ` { .node=${spec.nodeIndex}, .prop=${propEnum(spec.property)}, .fn=${spec.fnName}, .textFn=nullptr },`;
35
+ return ` { .node=${spec.nodeIndex}, .prop=${propEnum(spec.property)}, .fn=${spec.fnName}, .textFn=nullptr, .lastValue=0, .initialized=0 },`;
36
36
  }
37
37
  /** Emit a full binding table from a list of specs. */
38
38
  export function emitBindingTable(specs) {