@typecad/cuttlefish 0.1.0-alpha.2 → 1.0.0-alpha.11

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 (224) hide show
  1. package/README.md +10 -10
  2. package/dist/api/board-types.d.ts +1 -1
  3. package/dist/api/config.d.ts +38 -2
  4. package/dist/api/index.d.ts +1 -1
  5. package/dist/api/schema/types.d.ts +15 -0
  6. package/dist/api/shared/async-runtime-static.d.ts +12 -1
  7. package/dist/api/shared/async-runtime-static.js +69 -24
  8. package/dist/api/shared/async-symbol-detector.d.ts +11 -0
  9. package/dist/api/shared/async-symbol-detector.js +140 -0
  10. package/dist/api/shared/async-types.d.ts +24 -0
  11. package/dist/api/shared/coop-scheduler.d.ts +60 -0
  12. package/dist/api/shared/coop-scheduler.js +149 -0
  13. package/dist/api/shared/display-adapter.d.ts +4 -3
  14. package/dist/api/shared/display-adapter.js +15 -118
  15. package/dist/api/shared/display-adapters/sdl.js +38 -31
  16. package/dist/api/shared/display-profile.d.ts +47 -25
  17. package/dist/api/shared/display-profile.js +31 -164
  18. package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
  19. package/dist/api/shared/framework-manifest-registry.js +27 -0
  20. package/dist/api/shared/framework-manifest.d.ts +595 -0
  21. package/dist/api/shared/framework-manifest.js +193 -0
  22. package/dist/api/shared/glcdfont.d.ts +12 -0
  23. package/dist/api/shared/glcdfont.js +124 -0
  24. package/dist/api/shared/graphics-strategy.d.ts +37 -0
  25. package/dist/api/shared/hal-op-ir.d.ts +764 -1
  26. package/dist/api/shared/hal-op-ir.js +129 -1
  27. package/dist/api/shared/index.d.ts +22 -2
  28. package/dist/api/shared/index.js +26 -1
  29. package/dist/api/shared/ir-core.d.ts +4 -0
  30. package/dist/api/shared/ir-declarations.d.ts +6 -0
  31. package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
  32. package/dist/api/shared/native-display-op-resolver.js +64 -0
  33. package/dist/api/shared/platform-strategy.d.ts +119 -2
  34. package/dist/api/shared/polyfill-helper-registry.js +18 -1
  35. package/dist/api/shared/promise-runtime.d.ts +1 -1
  36. package/dist/api/shared/promise-runtime.js +95 -13
  37. package/dist/api/shared/spdx-licenses.d.ts +139 -0
  38. package/dist/api/shared/spdx-licenses.js +385 -0
  39. package/dist/api/shared/toolchain-types.d.ts +17 -0
  40. package/dist/api/shared/types.d.ts +8 -0
  41. package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
  42. package/dist/api/shared/validate-framework-manifest.js +494 -0
  43. package/dist/api/shared/worker-runtime-polyfill.d.ts +22 -0
  44. package/dist/api/shared/worker-runtime-polyfill.js +34 -0
  45. package/dist/api/shared/worker-runtime.d.ts +69 -0
  46. package/dist/api/shared/worker-runtime.js +163 -0
  47. package/dist/cli-utils.d.ts +1 -0
  48. package/dist/cli-utils.js +3 -1
  49. package/dist/cli.js +249 -32
  50. package/dist/config-loader.d.ts +7 -2
  51. package/dist/config-loader.js +23 -3
  52. package/dist/config-schema.d.ts +112 -70
  53. package/dist/config-schema.js +14 -0
  54. package/dist/contract/board-generator.d.ts +17 -0
  55. package/dist/contract/board-generator.js +57 -0
  56. package/dist/contract/contract-parser.d.ts +217 -0
  57. package/dist/contract/contract-parser.js +224 -0
  58. package/dist/contract/index.d.ts +22 -0
  59. package/dist/contract/index.js +68 -0
  60. package/dist/create/board-codegen.js +4 -4
  61. package/dist/create/board-generators.js +4 -5
  62. package/dist/create/board-spec.d.ts +72 -75
  63. package/dist/create/board-spec.js +0 -1
  64. package/dist/create/framework-catalog.d.ts +64 -0
  65. package/dist/create/framework-catalog.js +136 -0
  66. package/dist/create/index.d.ts +5 -1
  67. package/dist/create/index.js +3 -1
  68. package/dist/create/init-scaffold.d.ts +12 -3
  69. package/dist/create/init-scaffold.js +49 -14
  70. package/dist/create/init-templates.d.ts +4 -0
  71. package/dist/create/init-templates.js +271 -17
  72. package/dist/create/init-wizard.js +41 -27
  73. package/dist/create/install-deps.d.ts +32 -0
  74. package/dist/create/install-deps.js +46 -0
  75. package/dist/debug/preprocessor.js +178 -39
  76. package/dist/debug/types.d.ts +23 -0
  77. package/dist/diagnostics/mermaid-builder.d.ts +1 -1
  78. package/dist/diagnostics/mermaid-builder.js +34 -24
  79. package/dist/emit/compliance/arxml-writer.d.ts +11 -0
  80. package/dist/emit/compliance/arxml-writer.js +34 -0
  81. package/dist/emit/compliance/compliance-context.d.ts +57 -0
  82. package/dist/emit/compliance/compliance-context.js +113 -0
  83. package/dist/emit/compliance/deviation-ledger.d.ts +27 -0
  84. package/dist/emit/compliance/deviation-ledger.js +47 -0
  85. package/dist/emit/compliance/deviation-writer.d.ts +30 -0
  86. package/dist/emit/compliance/deviation-writer.js +37 -0
  87. package/dist/emit/compliance/index.d.ts +7 -0
  88. package/dist/emit/compliance/index.js +6 -0
  89. package/dist/emit/compliance/rule-engine.d.ts +13 -0
  90. package/dist/emit/compliance/rule-engine.js +101 -0
  91. package/dist/emit/compliance/rules.d.ts +16 -0
  92. package/dist/emit/compliance/rules.js +191 -0
  93. package/dist/emit/compliance/types.d.ts +59 -0
  94. package/dist/emit/compliance/types.js +8 -0
  95. package/dist/emit/cpp-emitter.js +4 -3
  96. package/dist/emit/emitters/class-emitter.js +6 -1
  97. package/dist/emit/emitters/emitter-context.d.ts +28 -1
  98. package/dist/emit/emitters/function-emitter-impl.js +153 -53
  99. package/dist/emit/emitters/line-appender.js +19 -0
  100. package/dist/emit/emitters/line-marker.d.ts +38 -0
  101. package/dist/emit/emitters/line-marker.js +39 -0
  102. package/dist/emit/emitters/output-finalizer.d.ts +6 -0
  103. package/dist/emit/emitters/output-finalizer.js +101 -12
  104. package/dist/emit/emitters/setup.d.ts +17 -0
  105. package/dist/emit/emitters/setup.js +372 -30
  106. package/dist/emit/emitters/top-level-prep.js +8 -0
  107. package/dist/emit/emitters/type-decl-emitter.js +28 -3
  108. package/dist/emit/emitters/ui-emitter.js +33 -9
  109. package/dist/emit/expression-renderer.d.ts +1 -1
  110. package/dist/emit/expression-renderer.js +91 -6
  111. package/dist/emit/route-hal-op.js +18 -5
  112. package/dist/emit/snprintf-helpers.js +15 -4
  113. package/dist/emit/statement-renderer.d.ts +10 -0
  114. package/dist/emit/statement-renderer.js +34 -5
  115. package/dist/emit/utils/async-state-machine.js +221 -125
  116. package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
  117. package/dist/emit/utils/hal-op-cpp-type.js +40 -0
  118. package/dist/framework-package.js +2 -0
  119. package/dist/framework-registry.d.ts +17 -0
  120. package/dist/install/framework-catalog.d.ts +53 -0
  121. package/dist/install/framework-catalog.js +107 -0
  122. package/dist/install/handle-install.d.ts +35 -0
  123. package/dist/install/handle-install.js +177 -0
  124. package/dist/install/index.d.ts +4 -0
  125. package/dist/install/index.js +3 -0
  126. package/dist/ir/adc-range-validation.js +40 -26
  127. package/dist/ir/build-ir-state.d.ts +1 -0
  128. package/dist/ir/build-ir-state.js +14 -0
  129. package/dist/ir/build-ir.js +9 -5
  130. package/dist/ir/call-graph.js +16 -0
  131. package/dist/ir/expression-to-ir.js +207 -6
  132. package/dist/ir/feature-registry.js +7 -25
  133. package/dist/ir/function-builder.js +22 -0
  134. package/dist/ir/hal/hal-emitter.d.ts +5 -2
  135. package/dist/ir/hal/hal-emitter.js +50 -18
  136. package/dist/ir/hal/hal-parser.d.ts +6 -0
  137. package/dist/ir/hal/hal-parser.js +81 -0
  138. package/dist/ir/hal/hal-plugins.js +733 -1
  139. package/dist/ir/identifier-collector.js +35 -0
  140. package/dist/ir/interrupt-analysis.d.ts +5 -1
  141. package/dist/ir/interrupt-analysis.js +16 -19
  142. package/dist/ir/memory-budget-validation.js +1 -0
  143. package/dist/ir/network-validation.d.ts +4 -0
  144. package/dist/ir/network-validation.js +184 -0
  145. package/dist/ir/ownership-analysis.js +39 -1
  146. package/dist/ir/peripheral-ownership.js +5 -0
  147. package/dist/ir/peripheral-validation.d.ts +1 -1
  148. package/dist/ir/peripheral-validation.js +6 -3
  149. package/dist/ir/pin-alias-conflict.d.ts +1 -1
  150. package/dist/ir/pin-alias-conflict.js +2 -1
  151. package/dist/ir/pin-capability-validation.js +71 -32
  152. package/dist/ir/pin-mode-validation.d.ts +2 -2
  153. package/dist/ir/pin-mode-validation.js +54 -18
  154. package/dist/ir/pin-safety.d.ts +1 -1
  155. package/dist/ir/pin-safety.js +2 -1
  156. package/dist/ir/program-analysis.d.ts +81 -0
  157. package/dist/ir/program-analysis.js +429 -0
  158. package/dist/ir/pulldown-validation.d.ts +1 -1
  159. package/dist/ir/pulldown-validation.js +2 -1
  160. package/dist/ir/pwm-timer-sharing.d.ts +1 -1
  161. package/dist/ir/pwm-timer-sharing.js +2 -1
  162. package/dist/ir/render-expr.js +11 -0
  163. package/dist/ir/resource-analysis.js +2 -0
  164. package/dist/ir/timer0-pwm-timing-conflict.d.ts +1 -1
  165. package/dist/ir/timer0-pwm-timing-conflict.js +2 -1
  166. package/dist/ir/timing-validation.d.ts +6 -1
  167. package/dist/ir/timing-validation.js +61 -13
  168. package/dist/ir/transformers/call-statement.js +105 -0
  169. package/dist/ir/transformers/expressions.js +62 -0
  170. package/dist/ir/transformers/hal-call-resolver.js +21 -0
  171. package/dist/ir/transformers/hal-emit-helpers.js +1 -1
  172. package/dist/ir/transformers/namespace-methods.js +17 -12
  173. package/dist/ir/transformers/ui-reactive.js +2 -2
  174. package/dist/ir/transformers/variables.js +137 -19
  175. package/dist/ir/try-catch-validation.js +2 -0
  176. package/dist/ir/type-resolution.js +20 -2
  177. package/dist/ir/ui-element-auto-wire.js +7 -5
  178. package/dist/ir/unit-suspicion-validation.js +9 -7
  179. package/dist/ir/utils/map-statements.d.ts +4 -0
  180. package/dist/ir/utils/map-statements.js +79 -0
  181. package/dist/ir/validation-orchestrator.js +15 -8
  182. package/dist/ir/worker-analysis.d.ts +10 -0
  183. package/dist/ir/worker-analysis.js +261 -0
  184. package/dist/libdef/c-to-decl.d.ts +27 -0
  185. package/dist/libdef/c-to-decl.js +397 -0
  186. package/dist/libdef/component-decls.d.ts +2 -0
  187. package/dist/libdef/component-decls.js +6 -0
  188. package/dist/libdef/component-discovery.d.ts +43 -0
  189. package/dist/libdef/component-discovery.js +83 -0
  190. package/dist/libdef/cpp-to-decl.d.ts +9 -0
  191. package/dist/libdef/cpp-to-decl.js +72 -0
  192. package/dist/libdef/registry.js +5 -2
  193. package/dist/lint-cache.d.ts +59 -0
  194. package/dist/lint-cache.js +257 -0
  195. package/dist/orchestrator/graph-builder.js +14 -9
  196. package/dist/platform/coop-scheduler-runtime.d.ts +19 -0
  197. package/dist/platform/coop-scheduler-runtime.js +41 -0
  198. package/dist/platform/generic-strategy.d.ts +15 -3
  199. package/dist/platform/generic-strategy.js +49 -4
  200. package/dist/safety/safety-bridge.d.ts +11 -0
  201. package/dist/safety/safety-bridge.js +48 -0
  202. package/dist/safety/sidecar-bridge.d.ts +5 -0
  203. package/dist/safety/sidecar-bridge.js +22 -0
  204. package/dist/safety-hook.d.ts +79 -0
  205. package/dist/safety-hook.js +35 -0
  206. package/dist/testing.d.ts +8 -2
  207. package/dist/testing.js +9 -2
  208. package/dist/transpile.d.ts +3 -0
  209. package/dist/transpile.js +191 -49
  210. package/dist/types.d.ts +22 -3
  211. package/dist/ui-hook.d.ts +17 -3
  212. package/dist/utils/cli.js +123 -2
  213. package/dist/utils/fs.d.ts +13 -0
  214. package/dist/utils/fs.js +50 -0
  215. package/dist/utils/ui.js +3 -1
  216. package/package.json +23 -3
  217. package/dist/api/shared/display-adapters/eink-mono.d.ts +0 -2
  218. package/dist/api/shared/display-adapters/eink-mono.js +0 -53
  219. package/dist/api/shared/display-adapters/ssd1309.d.ts +0 -2
  220. package/dist/api/shared/display-adapters/ssd1309.js +0 -136
  221. package/dist/api/shared/display-adapters/st7796.d.ts +0 -2
  222. package/dist/api/shared/display-adapters/st7796.js +0 -132
  223. package/dist/ir/heap-array-validation.d.ts +0 -24
  224. package/dist/ir/heap-array-validation.js +0 -29
@@ -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,24 +65,40 @@ 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,
101
+ filePath: stmt.sourceSpan?.filePath,
54
102
  code: 'blocking-delay-in-loop',
55
103
  source: 'timing-validation',
56
104
  });
@@ -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 {
@@ -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,54 @@ 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
+ // 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",
434
+ ]);
435
+ /**
436
+ * Rewrite an awaited hal-op statement (or a block whose LAST statement is
437
+ * one — chained HAL calls resolve to blocks of hal-ops) into an awaited
438
+ * `__WIFI_WAIT__` / `__HTTP_WAIT__` / `__HAL_WAIT__` marker call carrying the
439
+ * original op as a hal-expr argument. Returns undefined when the statement is
440
+ * not an awaitable HAL op.
441
+ */
442
+ function awaitedNetMarker(stmt) {
443
+ if (!stmt)
444
+ return undefined;
445
+ if (stmt.kind === "hal-op" && AWAITABLE_HAL_OPS.has(stmt.operation.operation)) {
446
+ const opName = stmt.operation.operation;
447
+ const callee = opName.startsWith("http.") ? "__HTTP_WAIT__"
448
+ : opName.startsWith("wifi.") ? "__WIFI_WAIT__"
449
+ : opName.startsWith("ble.") ? "__BLE_WAIT__"
450
+ : "__HAL_WAIT__";
451
+ return {
452
+ kind: "call",
453
+ sourceSpan: stmt.sourceSpan,
454
+ leadingComments: stmt.leadingComments,
455
+ trailingComments: stmt.trailingComments,
456
+ callee,
457
+ args: [{ kind: "hal-expr", operation: stmt.operation }],
458
+ isAwaited: true,
459
+ };
460
+ }
461
+ if (stmt.kind === "block" && stmt.body.length > 0) {
462
+ const last = awaitedNetMarker(stmt.body[stmt.body.length - 1]);
463
+ if (last) {
464
+ return { ...stmt, body: [...stmt.body.slice(0, -1), last] };
465
+ }
466
+ }
467
+ return undefined;
468
+ }
@@ -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
  }
@@ -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
  }
@@ -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) {
@@ -7,13 +7,18 @@ 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";
14
+ import { hasSafetyHook, requireSafetyHook } from "../../safety-hook.js";
13
15
  function replaceHalReadBufferPlaceholder(op, varName) {
14
16
  if (op.operation === "i2c.read_buffer" && op.buffer === "__HAL_READ_BUF__") {
15
17
  return { ...op, buffer: varName };
16
18
  }
19
+ if (op.operation === "spi.read_buffer" && op.buffer === "__HAL_READ_BUF__") {
20
+ return { ...op, buffer: varName };
21
+ }
17
22
  return op;
18
23
  }
19
24
  export function assignmentOperatorToString(kind) {
@@ -371,6 +376,56 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
371
376
  localVariableTypes.set(varName, cppType);
372
377
  continue;
373
378
  }
379
+ // ── safe.<method>(...) — @typecad/safety call as initializer.
380
+ // Handles expression-position safe.* calls like `const r = safe.read(pin)`.
381
+ // The statement-position form (safe.pinMode(...)) is handled in the
382
+ // call-statement transformer. Both produce hal-op IR nodes that
383
+ // routeHALOp() later dispatches to the safety hook.
384
+ if (hasSafetyHook() &&
385
+ ts.isCallExpression(declaration.initializer) &&
386
+ ts.isPropertyAccessExpression(declaration.initializer.expression) &&
387
+ ts.isIdentifier(declaration.initializer.expression.expression) &&
388
+ declaration.initializer.expression.expression.text === "safe") {
389
+ const method = declaration.initializer.expression.name.text;
390
+ const argValues = declaration.initializer.arguments.map((a) => {
391
+ if (ts.isNumericLiteral(a))
392
+ return Number(a.text);
393
+ if (ts.isStringLiteral(a))
394
+ return a.text;
395
+ if (a.kind === ts.SyntaxKind.TrueKeyword)
396
+ return true;
397
+ if (a.kind === ts.SyntaxKind.FalseKeyword)
398
+ return false;
399
+ if (ts.isIdentifier(a)) {
400
+ const name = a.text;
401
+ if (name === "INPUT")
402
+ return 0;
403
+ if (name === "OUTPUT")
404
+ return 1;
405
+ if (name === "INPUT_PULLUP")
406
+ return 2;
407
+ return name;
408
+ }
409
+ return undefined;
410
+ });
411
+ const op = requireSafetyHook().resolveSemanticCall?.(`safe.${method}`, argValues);
412
+ if (op) {
413
+ // Emit a var_decl initialized by a hal-expr carrying the safety op.
414
+ // routeHALOp() resolves it to the C++ expression at emit time.
415
+ const initExpr = { kind: "hal-expr", operation: op };
416
+ lowered.push({
417
+ kind: "var_decl",
418
+ name: varName,
419
+ storage,
420
+ cppType: "auto",
421
+ initializer: initExpr,
422
+ sourceSpan: makeSourceSpan(declaration, fileName, sourceText),
423
+ });
424
+ // auto-deduced to SafeReadResult on the C++ side.
425
+ localVariableTypes.set(varName, "auto");
426
+ continue;
427
+ }
428
+ }
374
429
  if (ts.isNewExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression)) {
375
430
  const className = declaration.initializer.expression.text;
376
431
  const ctorArgs = declaration.initializer.arguments;
@@ -405,9 +460,26 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
405
460
  }
406
461
  }
407
462
  }
463
+ // new HttpRequest(method, url) — positional ctor fields. The URL is
464
+ // stored as C++ expression text (quoted literal or identifier).
465
+ if (className === "HttpRequest" && ctorArgs && ctorArgs.length >= 2) {
466
+ const methodArg = ctorArgs[0];
467
+ if (ts.isStringLiteral(methodArg))
468
+ fieldValues.set("_method", methodArg.text.toUpperCase());
469
+ const urlText = httpUrlArgText(ctorArgs[1]);
470
+ if (urlText)
471
+ fieldValues.set("_url", urlText);
472
+ }
408
473
  if (ctorArgs) {
474
+ // Resolve a Pin-identifier ctor arg to its _pin number, for any HAL
475
+ // class whose first constructor field is _pin (Pin itself, plus
476
+ // pin-bearing wrappers like RmtChannel). Mirrors the Pin branch
477
+ // above but keyed off the class's registered ctor field map so new
478
+ // pin-keyed classes work without a per-class branch here.
479
+ const ctorFieldMap = getHALCtorFieldMap(className);
480
+ const firstFieldIsPin = ctorFieldMap && Array.from(ctorFieldMap.keys())[0] === "_pin";
409
481
  for (const arg of ctorArgs) {
410
- if (ts.isIdentifier(arg) && className === "Pin") {
482
+ if (ts.isIdentifier(arg) && (className === "Pin" || firstFieldIsPin)) {
411
483
  const existing = halInstances.get(arg.text);
412
484
  if (existing && existing.fieldValues.has("_pin")) {
413
485
  fieldValues.set("_pin", existing.fieldValues.get("_pin"));
@@ -430,6 +502,45 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
430
502
  const isSingletonReceiver = ts.isIdentifier(receiver) && isHALSingleton(receiver.text);
431
503
  if (result && (!isOwnershipMethod || isSingletonReceiver)) {
432
504
  const isHalOpReturn = result.returnValue === "__hal_op_return__";
505
+ // A __TYPED_ARRAY__ return comes ONLY from a HAL method body
506
+ // (`return new Uint8Array(count)` in e.g. I2CDevice.readBytes /
507
+ // SPIDevice.readRegister). The method's side-effect HAL ops (the
508
+ // i2c.read_buffer / spi.read_buffer fill loops) write INTO this buffer
509
+ // via the __HAL_READ_BUF__ placeholder (rewritten to `varName`). So the
510
+ // buffer var_decl MUST precede the fill ops in emitted order — at top
511
+ // level the var_decl is hoisted to file scope which masks this, but a
512
+ // function-local `const data = ...readBytes()` would otherwise emit the
513
+ // fill loop referencing `data` before its declaration. Detect the typed
514
+ // array up front so we can emit its declaration first.
515
+ const isTypedArrayReturn = typeof result.returnValue === "string" && result.returnValue.startsWith("__TYPED_ARRAY__:");
516
+ if (isTypedArrayReturn && typeof result.returnValue === "string") {
517
+ const parts = result.returnValue.split(":");
518
+ const elementType = parts[1];
519
+ const size = parts[2];
520
+ // Force non-const storage (the buffer is written by the fill op) and
521
+ // synthesize a zero-init array initializer so the var_decl renderer
522
+ // emits `T data[] = { 0, 0, ... }` — a bare `const T data[N];` that
523
+ // is later written would fail to compile (assignment to const).
524
+ // Mirrors plain `new Uint8Array(N)` (expression-to-ir.ts).
525
+ const count = parseInt(size, 10);
526
+ const initElements = !isNaN(count) && count > 0 && count <= 256
527
+ ? Array(count).fill(0).map(() => ({ kind: "number", value: 0 }))
528
+ : [];
529
+ lowered.push({
530
+ kind: "var_decl",
531
+ sourceSpan: makeSourceSpan(declaration, fileName, sourceText),
532
+ leadingComments: commentsAssigned ? [] : statementComments.leadingComments,
533
+ trailingComments: [],
534
+ name: varName,
535
+ storage: "let",
536
+ cppType: `${elementType}[${size}]`,
537
+ initializer: initElements.length > 0
538
+ ? { kind: "array", elements: initElements, elementType }
539
+ : undefined,
540
+ });
541
+ activeCArrayVars.add(varName);
542
+ commentsAssigned = true;
543
+ }
433
544
  if (result.halOps && result.halOps.length > 0) {
434
545
  const sideEffectOps = isHalOpReturn ? result.halOps.slice(0, -1) : result.halOps;
435
546
  const halStmts = sideEffectOps.map(op => ({
@@ -474,6 +585,19 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
474
585
  }
475
586
  }
476
587
  }
588
+ // For Http factory calls (const req = Http.get(url)), record the
589
+ // HTTP verb + URL so req.send() can resolve this._method/this._url.
590
+ if (result.returnClassName === "HttpRequest") {
591
+ const verb = httpFactoryVerb(init.expression.name.text);
592
+ if (verb)
593
+ fieldValues.set("_method", verb);
594
+ const urlArg = init.arguments?.[0];
595
+ if (urlArg) {
596
+ const urlText = httpUrlArgText(urlArg);
597
+ if (urlText)
598
+ fieldValues.set("_url", urlText);
599
+ }
600
+ }
477
601
  halInstances.set(varName, {
478
602
  className: result.returnClassName,
479
603
  fieldValues,
@@ -527,24 +651,18 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
527
651
  if (/\b\d+\.\d+\b/.test(result.returnValue)) {
528
652
  registerFloatVariable(varName);
529
653
  }
654
+ // __TYPED_ARRAY__ returns are handled above (declared BEFORE the
655
+ // fill ops). Everything else is a scalar/value return captured
656
+ // after the side-effect ops.
530
657
  const isTypedArray = result.returnValue.startsWith("__TYPED_ARRAY__:");
531
- if (isTypedArray) {
532
- const parts = result.returnValue.split(":");
533
- const elementType = parts[1];
534
- const size = parts[2];
535
- lowered.push({
536
- kind: "var_decl",
537
- sourceSpan: makeSourceSpan(declaration, fileName, sourceText),
538
- leadingComments: commentsAssigned ? [] : statementComments.leadingComments,
539
- trailingComments: [],
540
- name: varName,
541
- storage,
542
- cppType: `${elementType}[${size}]`,
543
- initializer: undefined,
544
- });
545
- activeCArrayVars.add(varName);
546
- }
547
- else {
658
+ // A factory returning a compile-time HAL instance (e.g.
659
+ // `const req = Http.get(url)` → returnValue `new HttpRequest(...)`)
660
+ // is fully tracked via halInstances — every later method call on
661
+ // the variable resolves at compile time. Emitting the raw
662
+ // `new HttpRequest(...)` would reference a class that doesn't
663
+ // exist in the C++ output.
664
+ const isHalInstanceReturn = !!result.returnClassName && /^new\s/.test(result.returnValue.trim());
665
+ if (!isTypedArray && !isHalInstanceReturn) {
548
666
  lowered.push({
549
667
  kind: "var_decl",
550
668
  sourceSpan: makeSourceSpan(declaration, fileName, sourceText),
@@ -39,6 +39,7 @@ export function validateTryCatch(program, boardConstants, strategy) {
39
39
  hint: `function readSensor(): number | null {\n const data = sensor.read();\n if (!data) return null; // error path\n return data.value; // success path\n}`,
40
40
  line: stmt.sourceSpan?.startLine,
41
41
  column: stmt.sourceSpan?.startColumn,
42
+ filePath: stmt.sourceSpan?.filePath,
42
43
  source: 'try-catch-validation',
43
44
  });
44
45
  // Still recurse into nested blocks to find other try/catch
@@ -57,6 +58,7 @@ export function validateTryCatch(program, boardConstants, strategy) {
57
58
  hint: `return null; // or return an error code`,
58
59
  line: stmt.sourceSpan?.startLine,
59
60
  column: stmt.sourceSpan?.startColumn,
61
+ filePath: stmt.sourceSpan?.filePath,
60
62
  source: 'try-catch-validation',
61
63
  });
62
64
  return;