@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
@@ -4,6 +4,7 @@
4
4
  * Extracted from cpp-emitter.ts
5
5
  */
6
6
  import { escapeCppStringLiteral } from "../../utils/strings.js";
7
+ import { routeHALOp } from "../route-hal-op.js";
7
8
  /**
8
9
  * Converts a string to PascalCase.
9
10
  * Used for generating class names from function names.
@@ -53,6 +54,20 @@ export function generateAsyncTaskClass(fnName, fnStatements, strategy, knownFunc
53
54
  segments.push({ preStatements: currentPre, awaitedCallee: stmt.callee, awaitedArgs: stmt.args });
54
55
  currentPre = [];
55
56
  }
57
+ else if (
58
+ // Chained HAL calls (e.g. `await Http.get(url).send()`) resolve to a
59
+ // block of hal-ops whose LAST statement is the awaited wait marker.
60
+ // Flatten it so the split sees the marker; the leading ops (begin,
61
+ // setters) run as pre-statements of the same segment.
62
+ stmt.kind === "block" &&
63
+ stmt.body.length > 0 &&
64
+ stmt.body[stmt.body.length - 1].kind === "call" &&
65
+ stmt.body[stmt.body.length - 1].isAwaited) {
66
+ const last = stmt.body[stmt.body.length - 1];
67
+ currentPre.push(...stmt.body.slice(0, -1));
68
+ segments.push({ preStatements: currentPre, awaitedCallee: last.callee, awaitedArgs: last.args });
69
+ currentPre = [];
70
+ }
56
71
  else {
57
72
  currentPre.push(stmt);
58
73
  }
@@ -61,11 +76,23 @@ export function generateAsyncTaskClass(fnName, fnStatements, strategy, knownFunc
61
76
  segments.push({ preStatements: currentPre, awaitedArgs: [] });
62
77
  const awaitCount = segments.filter((s) => s.awaitedCallee !== undefined).length;
63
78
  const stateCount = awaitCount + 1; // STATE_0 … STATE_{awaitCount}; cyclic loops back, linear adds STATE_DONE
79
+ // State enum is emitted as `enum class State` (AUTOSAR A7-2-1), so all
80
+ // references must be scope-qualified as `State::STATE_X`. The stateEnumList
81
+ // (used inside `enum class State { ... }`) uses the bare names; everywhere
82
+ // else (assignments, case labels, comparisons) uses the qualified form.
64
83
  const stateNames = [];
65
- for (let i = 0; i < stateCount; i++)
84
+ const qualifiedStateNames = [];
85
+ for (let i = 0; i < stateCount; i++) {
66
86
  stateNames.push(`STATE_${i}`);
67
- if (!isCyclic)
87
+ qualifiedStateNames.push(`State::STATE_${i}`);
88
+ }
89
+ if (!isCyclic) {
68
90
  stateNames.push("STATE_DONE");
91
+ qualifiedStateNames.push("State::STATE_DONE");
92
+ }
93
+ const Q = (i) => qualifiedStateNames[i];
94
+ const Q_DONE = "State::STATE_DONE";
95
+ const Q_0 = "State::STATE_0";
69
96
  // Helper: render a non-await statement as a single C++ line
70
97
  const renderStmt = (stmt) => renderStatement(stmt, false, strategy, undefined, undefined, knownFunctionReturnTypes);
71
98
  // Collect edge-detection markers from segments
@@ -101,16 +128,74 @@ export function generateAsyncTaskClass(fnName, fnStatements, strategy, knownFunc
101
128
  tapMembers.set(member, i);
102
129
  }
103
130
  }
131
+ // Collect net-wait (WiFi/HTTP/HAL) markers: awaited hal-ops rewritten to
132
+ // __WIFI_WAIT__/__HTTP_WAIT__/__HAL_WAIT__ calls carrying the op as a
133
+ // hal-expr arg. The strategy lowers the op to start code + poll condition.
134
+ const netInfoMap = new Map();
135
+ for (let i = 0; i < segments.length; i++) {
136
+ const seg = segments[i];
137
+ if ((seg.awaitedCallee === "__WIFI_WAIT__" || seg.awaitedCallee === "__HTTP_WAIT__" || seg.awaitedCallee === "__BLE_WAIT__" || seg.awaitedCallee === "__HAL_WAIT__") &&
138
+ seg.awaitedArgs[0]?.kind === "hal-expr") {
139
+ const op = seg.awaitedArgs[0].operation;
140
+ netInfoMap.set(i, netWaitInfo(op, strategy));
141
+ }
142
+ }
143
+ // ── Render each state ──────────────────────────────────────────────────
144
+ // Decomposition: a state's ENTRY condition comes from the await that ended
145
+ // the PREVIOUS segment (edge / tap / net poll / timed deadline); once the
146
+ // condition fires, the segment body runs and the await ending THIS segment
147
+ // is armed (pin snapshot / tap snapshot / net start op / deadline).
148
+ // Lines that arm the await ending segment i and advance the state.
149
+ const armNextLines = (i, pad) => {
150
+ const seg = segments[i];
151
+ const lines = [];
152
+ if (seg.awaitedCallee === undefined) {
153
+ lines.push(`${pad}_state = ${isCyclic ? Q_0 : Q_DONE};`);
154
+ return lines;
155
+ }
156
+ const edge = edgeInfoMap.get(i);
157
+ const tap = tapInfoMap.get(i);
158
+ const net = netInfoMap.get(i);
159
+ if (edge) {
160
+ lines.push(`${pad}_edgePrev_p${edge.pin} = ${strategy.readDigitalPin?.(String(edge.pin)) ?? `digitalRead(${edge.pin})`};`);
161
+ if (edge.timeout !== null) {
162
+ lines.push(`${pad}_waitUntil = ${strategy.currentTimeMillis()} + ${edge.timeout};`);
163
+ }
164
+ }
165
+ else if (tap) {
166
+ lines.push(`${pad}_tapPrev_${i} = __ui_tap_seq;`);
167
+ }
168
+ else if (net) {
169
+ for (const startLine of net.startLines)
170
+ lines.push(`${pad}${startLine}`);
171
+ if (net.timeoutExpr !== null) {
172
+ lines.push(`${pad}_waitUntil = ${strategy.currentTimeMillis()} + ${net.timeoutExpr};`);
173
+ }
174
+ }
175
+ else {
176
+ // Plain timed wait (await delay(ms)) — arm its deadline.
177
+ const ms = seg.awaitedArgs[0] ? renderExpression(seg.awaitedArgs[0], strategy) : "0";
178
+ lines.push(`${pad}_waitUntil = ${strategy.currentTimeMillis()} + ${ms};`);
179
+ }
180
+ lines.push(`${pad}_state = ${Q(i + 1)};`);
181
+ return lines;
182
+ };
104
183
  const caseLines = [];
105
184
  for (let i = 0; i < segments.length; i++) {
106
185
  const seg = segments[i];
107
- const stateName = `STATE_${i}`;
108
- const isTerminal = seg.awaitedCallee === undefined;
186
+ const stateName = Q(i);
109
187
  const body = [];
110
- const edgeSetup = edgeInfoMap.get(i);
111
188
  const edgePoll = i > 0 ? edgeInfoMap.get(i - 1) : undefined;
112
- const tapSetup = tapInfoMap.get(i);
113
189
  const tapPoll = i > 0 ? tapInfoMap.get(i - 1) : undefined;
190
+ const netPoll = i > 0 ? netInfoMap.get(i - 1) : undefined;
191
+ // Segment body + arming of the next await, at the given indent.
192
+ const runSegment = (pad) => {
193
+ const lines = [];
194
+ for (const stmt of seg.preStatements)
195
+ lines.push(`${pad}${renderStmt(stmt)}`);
196
+ lines.push(...armNextLines(i, pad));
197
+ return lines;
198
+ };
114
199
  if (edgePoll) {
115
200
  // Poll state: check pin transition via digitalRead
116
201
  const prevVar = `_edgePrev_p${edgePoll.pin}`;
@@ -121,56 +206,15 @@ export function generateAsyncTaskClass(fnName, fnStatements, strategy, knownFunc
121
206
  ? `(${cond}) || ${strategy.currentTimeMillis()} >= _waitUntil`
122
207
  : cond;
123
208
  body.push(` {`);
124
- body.push(` int _cur = digitalRead(${edgePoll.pin});`);
209
+ body.push(` int _cur = ${strategy.readDigitalPin?.(String(edgePoll.pin)) ?? `digitalRead(${edgePoll.pin})`};`);
125
210
  body.push(` if (${fullCond}) {`);
126
211
  body.push(` ${prevVar} = _cur;`);
127
- for (const stmt of seg.preStatements)
128
- body.push(` ${renderStmt(stmt)}`);
129
- if (edgeSetup) {
130
- // Next await is also edge detection — set up its pin tracking
131
- body.push(` _edgePrev_p${edgeSetup.pin} = digitalRead(${edgeSetup.pin});`);
132
- if (edgeSetup.timeout !== null) {
133
- body.push(` _waitUntil = ${strategy.currentTimeMillis()} + ${edgeSetup.timeout};`);
134
- }
135
- body.push(` _state = STATE_${i + 1};`);
136
- }
137
- else if (!isTerminal) {
138
- const ms = seg.awaitedArgs[0] ? renderExpression(seg.awaitedArgs[0], strategy) : "0";
139
- body.push(` _waitUntil = ${strategy.currentTimeMillis()} + ${ms};`);
140
- body.push(` _state = STATE_${i + 1};`);
141
- }
142
- else {
143
- body.push(` _state = ${isCyclic ? "STATE_0" : "STATE_DONE"};`);
144
- }
212
+ body.push(...runSegment(` `));
145
213
  body.push(` } else {`);
146
214
  body.push(` ${prevVar} = _cur;`);
147
215
  body.push(` }`);
148
216
  body.push(` }`);
149
217
  }
150
- else if (edgeSetup) {
151
- // Setup state: capture initial pin state before polling
152
- const prevVar = `_edgePrev_p${edgeSetup.pin}`;
153
- if (i === 0) {
154
- for (const stmt of seg.preStatements)
155
- body.push(` ${renderStmt(stmt)}`);
156
- body.push(` ${prevVar} = digitalRead(${edgeSetup.pin});`);
157
- if (edgeSetup.timeout !== null) {
158
- body.push(` _waitUntil = ${strategy.currentTimeMillis()} + ${edgeSetup.timeout};`);
159
- }
160
- body.push(` _state = STATE_${i + 1};`);
161
- }
162
- else {
163
- body.push(` if (${strategy.currentTimeMillis()} >= _waitUntil) {`);
164
- for (const stmt of seg.preStatements)
165
- body.push(` ${renderStmt(stmt)}`);
166
- body.push(` ${prevVar} = digitalRead(${edgeSetup.pin});`);
167
- if (edgeSetup.timeout !== null) {
168
- body.push(` _waitUntil = ${strategy.currentTimeMillis()} + ${edgeSetup.timeout};`);
169
- }
170
- body.push(` _state = STATE_${i + 1};`);
171
- body.push(` }`);
172
- }
173
- }
174
218
  else if (tapPoll) {
175
219
  // Tap-poll state: a previous segment ended with `await ui.onTap()`.
176
220
  // Wait until __ui_tap_seq bumps from the captured snapshot. A per-node
@@ -181,92 +225,42 @@ export function generateAsyncTaskClass(fnName, fnStatements, strategy, knownFunc
181
225
  ? `__ui_tap_seq != ${prevVar}`
182
226
  : `(__ui_tap_seq != ${prevVar}) && (__ui_tap_node == ${nodeFilter})`;
183
227
  body.push(` if (${cond}) {`);
184
- for (const stmt of seg.preStatements)
185
- body.push(` ${renderStmt(stmt)}`);
186
- // What does the NEXT await (ending this segment) look like? Re-derive
187
- // from the maps via a fresh lookup so TS doesn't narrow edgeSetup away.
188
- const nextEdge = edgeInfoMap.get(i) ?? null;
189
- if (tapSetup) {
190
- // Next await is also a tap — capture its snapshot now.
191
- body.push(` _tapPrev_${i} = __ui_tap_seq;`);
192
- body.push(` _state = STATE_${i + 1};`);
193
- }
194
- else if (nextEdge) {
195
- body.push(` _edgePrev_p${nextEdge.pin} = digitalRead(${nextEdge.pin});`);
196
- if (nextEdge.timeout !== null) {
197
- body.push(` _waitUntil = ${strategy.currentTimeMillis()} + ${nextEdge.timeout};`);
198
- }
199
- body.push(` _state = STATE_${i + 1};`);
200
- }
201
- else if (!isTerminal) {
202
- // Next await is a normal timed wait (delay) — arm its deadline.
203
- const ms = seg.awaitedArgs[0] ? renderExpression(seg.awaitedArgs[0], strategy) : "0";
204
- body.push(` _waitUntil = ${strategy.currentTimeMillis()} + ${ms};`);
205
- body.push(` _state = STATE_${i + 1};`);
206
- }
207
- else {
208
- body.push(` _state = ${isCyclic ? "STATE_0" : "STATE_DONE"};`);
209
- }
228
+ body.push(...runSegment(` `));
210
229
  body.push(` }`);
211
230
  }
212
- else if (tapSetup && !edgePoll) {
213
- // Tap-setup state: this segment ends with `await ui.onTap()`. Capture the
214
- // current tap counter so the poll state can detect the NEXT bump.
215
- // (edgePoll takes precedence when the previous await was an edge.)
216
- if (i === 0) {
217
- for (const stmt of seg.preStatements)
218
- body.push(` ${renderStmt(stmt)}`);
219
- body.push(` _tapPrev_${i} = __ui_tap_seq;`);
220
- body.push(` _state = STATE_${i + 1};`);
221
- }
222
- else {
223
- body.push(` if (${strategy.currentTimeMillis()} >= _waitUntil) {`);
224
- for (const stmt of seg.preStatements)
225
- body.push(` ${renderStmt(stmt)}`);
226
- body.push(` _tapPrev_${i} = __ui_tap_seq;`);
227
- body.push(` _state = STATE_${i + 1};`);
228
- body.push(` }`);
229
- }
231
+ else if (netPoll) {
232
+ // Net-poll state: a previous segment ended with an awaited WiFi/HTTP
233
+ // op. Wait until its poll condition fires (or its deadline expires).
234
+ const deadline = `${strategy.currentTimeMillis()} >= _waitUntil`;
235
+ const cond = netPoll.pollCond === null
236
+ ? deadline
237
+ : netPoll.timeoutExpr !== null
238
+ ? `(${netPoll.pollCond}) || ${deadline}`
239
+ : netPoll.pollCond;
240
+ body.push(` if (${cond}) {`);
241
+ body.push(...runSegment(` `));
242
+ body.push(` }`);
243
+ }
244
+ else if (i === 0) {
245
+ // First state runs immediately.
246
+ body.push(...runSegment(` `));
230
247
  }
231
248
  else {
232
- // Normal timed-wait state
233
- if (i === 0) {
234
- for (const stmt of seg.preStatements)
235
- body.push(` ${renderStmt(stmt)}`);
236
- if (!isTerminal) {
237
- const ms = seg.awaitedArgs[0] ? renderExpression(seg.awaitedArgs[0], strategy) : "0";
238
- body.push(` _waitUntil = ${strategy.currentTimeMillis()} + ${ms};`);
239
- body.push(` _state = STATE_${i + 1};`);
240
- }
241
- else {
242
- body.push(` _state = ${isCyclic ? "STATE_0" : "STATE_DONE"};`);
243
- }
244
- }
245
- else {
246
- body.push(` if (${strategy.currentTimeMillis()} >= _waitUntil) {`);
247
- for (const stmt of seg.preStatements)
248
- body.push(` ${renderStmt(stmt)}`);
249
- if (!isTerminal) {
250
- const ms = seg.awaitedArgs[0] ? renderExpression(seg.awaitedArgs[0], strategy) : "0";
251
- body.push(` _waitUntil = ${strategy.currentTimeMillis()} + ${ms};`);
252
- body.push(` _state = STATE_${i + 1};`);
253
- }
254
- else {
255
- body.push(` _state = ${isCyclic ? "STATE_0" : "STATE_DONE"};`);
256
- }
257
- body.push(` }`);
258
- }
249
+ // Previous await was a plain timed wait — poll its deadline.
250
+ body.push(` if (${strategy.currentTimeMillis()} >= _waitUntil) {`);
251
+ body.push(...runSegment(` `));
252
+ body.push(` }`);
259
253
  }
260
254
  caseLines.push(` case ${stateName}:`, ` {`, ...body, ` }`, ` break;`);
261
255
  }
262
256
  if (!isCyclic)
263
- caseLines.push(` case STATE_DONE:`, ` break;`);
257
+ caseLines.push(` case ${Q_DONE}:`, ` break;`);
264
258
  const stateEnumList = stateNames.join(", ");
265
- const isCompleteExpr = isCyclic ? "false" : "_state == STATE_DONE";
259
+ const isCompleteExpr = isCyclic ? "false" : `_state == ${Q_DONE}`;
266
260
  // Build constructor initializer list and edge/tap member declarations
267
261
  const edgeMemberArr = Array.from(edgeMembers);
268
262
  const tapMemberArr = Array.from(tapMembers.keys());
269
- const inits = [`_state(STATE_0)`, `_waitUntil(0)`];
263
+ const inits = [`_state(${Q_0})`, `_waitUntil(0)`];
270
264
  for (const m of edgeMemberArr)
271
265
  inits.push(`${m}(LOW)`);
272
266
  for (const m of tapMemberArr)
@@ -281,7 +275,7 @@ export function generateAsyncTaskClass(fnName, fnStatements, strategy, knownFunc
281
275
  `// Async state machine for ${fnName}`,
282
276
  `class ${className} {`,
283
277
  `public:`,
284
- ` enum State { ${stateEnumList} };`,
278
+ ` enum class State { ${stateEnumList} };`,
285
279
  ` ${className}() : ${ctorInitList} {}`,
286
280
  ` void run() {`,
287
281
  ` switch (_state) {`,
@@ -289,7 +283,7 @@ export function generateAsyncTaskClass(fnName, fnStatements, strategy, knownFunc
289
283
  ` }`,
290
284
  ` }`,
291
285
  ` bool isComplete() const { return ${isCompleteExpr}; }`,
292
- ` void reset() { _state = STATE_0; _waitUntil = 0;${edgeResetList}${tapResetList} }`,
286
+ ` void reset() { _state = ${Q_0}; _waitUntil = 0;${edgeResetList}${tapResetList} }`,
293
287
  `private:`,
294
288
  ` State _state;`,
295
289
  ` unsigned long _waitUntil;`,
@@ -331,6 +325,108 @@ function renderExpression(expr, strategy) {
331
325
  return "/* complex expr */";
332
326
  }
333
327
  }
328
+ /** Numeric-or-rendered HAL field → C++ text. */
329
+ function ms(v) {
330
+ return String(v);
331
+ }
332
+ /** True when a timeout field is the literal 0 (wait forever, no deadline). */
333
+ function isZeroTimeout(v) {
334
+ return v === undefined || v === null || String(v).trim() === "0";
335
+ }
336
+ /**
337
+ * Map an awaitable HAL op to its start statements + poll condition, lowering
338
+ * the start op and poll predicate through the platform strategy. Keep the set
339
+ * of handled ops in sync with AWAITABLE_HAL_OPS in ir/transformers/expressions.ts.
340
+ *
341
+ * Falls back to running the blocking form as the "start" with an immediate
342
+ * completion when the strategy can't lower the split ops.
343
+ */
344
+ function netWaitInfo(op, strategy) {
345
+ const o = op;
346
+ const route = (routedOp) => routeHALOp(routedOp, strategy);
347
+ const expr = (operation) => route({ operation })?.expression ?? null;
348
+ switch (op.operation) {
349
+ case "timing.delay":
350
+ return { startLines: [], pollCond: null, timeoutExpr: ms(o.ms) };
351
+ case "wifi.connect": {
352
+ const start = route({ operation: "wifi.connect_start", ssid: o.ssid, password: o.password });
353
+ const poll = expr("wifi.is_connected");
354
+ if (start?.code && poll) {
355
+ return {
356
+ startLines: [start.code],
357
+ pollCond: poll,
358
+ timeoutExpr: isZeroTimeout(o.timeoutMs) ? null : ms(o.timeoutMs),
359
+ };
360
+ }
361
+ break;
362
+ }
363
+ case "wifi.wait_connected": {
364
+ const poll = expr("wifi.is_connected");
365
+ if (poll) {
366
+ return {
367
+ startLines: [],
368
+ pollCond: poll,
369
+ timeoutExpr: isZeroTimeout(o.timeoutMs) ? null : ms(o.timeoutMs),
370
+ };
371
+ }
372
+ break;
373
+ }
374
+ case "wifi.wait_disconnected": {
375
+ const poll = expr("wifi.is_connected");
376
+ if (poll) {
377
+ return { startLines: [], pollCond: `!${poll}`, timeoutExpr: null };
378
+ }
379
+ break;
380
+ }
381
+ case "wifi.scan": {
382
+ const start = route({ operation: "wifi.scan_start" });
383
+ const poll = expr("wifi.scan_done");
384
+ if (start?.code && poll) {
385
+ return { startLines: [start.code], pollCond: poll, timeoutExpr: null };
386
+ }
387
+ break;
388
+ }
389
+ case "http.send": {
390
+ const start = route({ operation: "http.send_start" });
391
+ const poll = expr("http.done");
392
+ if (start?.code && poll) {
393
+ return { startLines: [start.code], pollCond: poll, timeoutExpr: null };
394
+ }
395
+ break;
396
+ }
397
+ case "ble.until_connected": {
398
+ const start = route({ operation: "ble.until_connected_start" });
399
+ const poll = expr("ble.is_connected");
400
+ if (start?.code && poll) {
401
+ return {
402
+ startLines: [start.code],
403
+ pollCond: poll,
404
+ timeoutExpr: isZeroTimeout(o.timeoutMs) ? null : ms(o.timeoutMs),
405
+ };
406
+ }
407
+ break;
408
+ }
409
+ case "worker.submit": {
410
+ // Worker offload (Phase 3). `await worker.submit(fn, arg)` rewrites to:
411
+ // submit the worker (start), then poll worker.done on the same handle.
412
+ // The submit op is emitted verbatim as the start line; the poll predicate
413
+ // is worker.done(handle) carrying the SAME handleId. This is a predicate-
414
+ // poll resume (same per-frame cost as net/timer awaits), NOT an ISR-driven
415
+ // resume — that defers with DMA-backed awaits.
416
+ const submit = routeHALOp(op, strategy);
417
+ // Carry the handle through to the poll so submit + done agree on the slot.
418
+ const poll = route({ operation: "worker.done", handleId: o.handleId })?.expression ?? null;
419
+ if (submit?.code && poll) {
420
+ return { startLines: [submit.code], pollCond: poll, timeoutExpr: null };
421
+ }
422
+ break;
423
+ }
424
+ }
425
+ // Fallback: run the blocking form immediately and complete on the next tick.
426
+ const blocking = routeHALOp(op, strategy);
427
+ const line = blocking?.code ?? (blocking?.expression ? `${blocking.expression};` : `/* unhandled awaited hal-op: ${op.operation} */`);
428
+ return { startLines: [line], pollCond: null, timeoutExpr: "0" };
429
+ }
334
430
  function parseEdgeMarker(argText) {
335
431
  const match = argText.match(/^__EDGE_(RISING|FALLING)__([a-zA-Z0-9_]+)__T(.*)$/);
336
432
  if (!match)
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Best-effort C++ return type for a HAL expression op.
3
+ * Used by snprintf format inference and expression type inference so
4
+ * string-returning ops (e.g. wifi.scan_ssid → const char*) use %s, not %d.
5
+ */
6
+ export declare function cppTypeForHalOp(operation: string): string | undefined;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Best-effort C++ return type for a HAL expression op.
3
+ * Used by snprintf format inference and expression type inference so
4
+ * string-returning ops (e.g. wifi.scan_ssid → const char*) use %s, not %d.
5
+ */
6
+ export function cppTypeForHalOp(operation) {
7
+ switch (operation) {
8
+ case "wifi.local_ip":
9
+ case "wifi.mac":
10
+ case "wifi.ap_ip":
11
+ case "wifi.scan_ssid":
12
+ case "http.body":
13
+ case "http.response_header":
14
+ return "const char*";
15
+ case "wifi.is_connected":
16
+ case "wifi.connect":
17
+ case "wifi.wait_connected":
18
+ case "wifi.scan_done":
19
+ case "wifi.ap_start":
20
+ case "wifi.connect_saved":
21
+ case "http.ok":
22
+ case "http.done":
23
+ return "bool";
24
+ case "wifi.status":
25
+ case "wifi.rssi":
26
+ case "wifi.scan":
27
+ case "wifi.scan_count":
28
+ case "wifi.scan_rssi":
29
+ case "wifi.scan_encryption":
30
+ case "wifi.scan_channel":
31
+ case "wifi.ap_client_count":
32
+ case "http.status":
33
+ case "http.content_length":
34
+ return "int";
35
+ case "http.send":
36
+ return "bool";
37
+ default:
38
+ return undefined;
39
+ }
40
+ }
@@ -50,6 +50,8 @@ export function loadFrameworkPackage(packageName, fromDir = process.cwd()) {
50
50
  } : undefined,
51
51
  classNameMapBuilder: typeof mod.buildClassNameMap === "function" ? mod.buildClassNameMap : undefined,
52
52
  libDeclGenerator: typeof mod.tryGenerateLibDecl === "function" ? mod.tryGenerateLibDecl : undefined,
53
+ doctor: typeof mod.doctor === "function" ? mod.doctor : undefined,
54
+ licenses: typeof mod.licenses === "function" ? mod.licenses : undefined,
53
55
  };
54
56
  setLoadedFramework(framework);
55
57
  registerPlatformStrategy(strategy);
@@ -14,6 +14,16 @@ export interface LoadedFramework {
14
14
  classNameMapBuilder?: (imports: any[]) => Map<string, string>;
15
15
  /** Framework library .d.ts declaration generation */
16
16
  libDeclGenerator?: (modulePath: string, file: string) => string | undefined;
17
+ /**
18
+ * Optional subcommand presenters owned by the framework. Cuttlefish dispatches
19
+ * `cuttlefish doctor` / `cuttlefish licenses` to these when the loaded
20
+ * framework provides them; otherwise it prints a no-support message. Each
21
+ * framework decides what (if anything) these do — e.g. framework-arduino
22
+ * checks arduino-cli + board core (doctor) and scans Arduino library licenses
23
+ * (licenses).
24
+ */
25
+ doctor?: () => void;
26
+ licenses?: (strict: boolean, all: boolean) => void;
17
27
  }
18
28
  /**
19
29
  * Resolves framework-specific library imports (e.g., Arduino libraries).
@@ -32,6 +42,13 @@ export interface FrameworkToolchain {
32
42
  compile(options: ToolchainOptions): CompileResult;
33
43
  upload?(options: ToolchainOptions): UploadResult;
34
44
  monitor?(options: ToolchainOptions): void;
45
+ /**
46
+ * Optional: launch an interactive debugger session for the last build
47
+ * (e.g. `west debug`). Not invoked by the standard build/compile flow;
48
+ * powers an explicit debug-attach entry point. Frameworks that support
49
+ * source-level debugging implement this to spawn their native debugger.
50
+ */
51
+ debug?(options: ToolchainOptions): void;
35
52
  }
36
53
  export declare function setLoadedFramework(framework: LoadedFramework): void;
37
54
  export declare function getLoadedFramework(): LoadedFramework;
@@ -4,6 +4,7 @@
4
4
  // Detects comparisons against analogRead() values that exceed the board's
5
5
  // ADC resolution (e.g., comparing to > 1023 on a 10-bit Arduino Uno).
6
6
  // ---------------------------------------------------------------------------
7
+ import { hasLoadedFramework, getLoadedFramework } from '../framework-registry.js';
7
8
  /**
8
9
  * Get ADC configuration from board constants.
9
10
  */
@@ -41,7 +42,19 @@ function isAnalogRead(expr) {
41
42
  return op === 'adc.read' || op === 'adc.read_voltage';
42
43
  }
43
44
  if (expr.kind === 'raw') {
44
- return /analogRead\s*\(/.test(expr.value);
45
+ const value = expr.value;
46
+ // Ask the loaded framework which call names produce an ADC read; fall back
47
+ // to the Wiring-derived analogRead when no framework is loaded.
48
+ const frameworkNames = hasLoadedFramework()
49
+ ? getLoadedFramework().strategy.analogReadCallNames?.()
50
+ : undefined;
51
+ const names = frameworkNames ?? new Set(['analogRead']);
52
+ for (const name of names) {
53
+ const re = new RegExp(`${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`);
54
+ if (re.test(value))
55
+ return true;
56
+ }
57
+ return false;
45
58
  }
46
59
  return false;
47
60
  }
@@ -78,6 +78,7 @@ export declare class CompilationContext {
78
78
  floatVariables: Set<string>;
79
79
  snprintfCounter: number;
80
80
  callbackPlaceholderCounter: number;
81
+ bleCharCounter: number;
81
82
  activeStrategy: PlatformStrategy | null;
82
83
  restParamFunctions: Map<string, string>;
83
84
  /**
@@ -65,6 +65,12 @@ export class CompilationContext {
65
65
  this.floatVariables = new Set();
66
66
  this.snprintfCounter = 0;
67
67
  this.callbackPlaceholderCounter = 0;
68
+ // BLE characteristic index counter — persists across separate Ble.server()
69
+ // calls so a multi-characteristic server (one server() per char, the common
70
+ // ble-demo pattern) gets unique sequential indices instead of every char
71
+ // clobbering slot 0. Read by the server()/characteristic() resolver branches
72
+ // in hal-parser.ts. Reset per file in hal-emitter.ts with the other counters.
73
+ this.bleCharCounter = 0;
68
74
  this.activeStrategy = null;
69
75
  this.restParamFunctions = new Map();
70
76
  /**
@@ -246,6 +252,14 @@ export function setCurrentBoardConstants(v) {
246
252
  pinAliasMap.set(name, num);
247
253
  mcuPinForwardMap.set(name, num);
248
254
  mcuPinReverseMap.set(num, name);
255
+ // Register an identifier-safe variant so that pins whose canonical
256
+ // name is not a legal JS identifier (e.g. nRF52840 "P0.28") can be
257
+ // imported under their underscore form (P0_28). See "Pin Naming
258
+ // Conventions" in the root AGENTS.md.
259
+ const identName = name.replace(/[.\s-]/g, "_");
260
+ if (identName !== name) {
261
+ pinAliasMap.set(identName, num);
262
+ }
249
263
  }
250
264
  }
251
265
  }
@@ -144,7 +144,11 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
144
144
  const parentStrategy = getContext().activeStrategy;
145
145
  return contextStorage.run(new CompilationContext(), () => {
146
146
  getContext().activeStrategy = parentStrategy;
147
- loadHALModules(true); // Parse HAL source files (force reload to pick up changes)
147
+ // Ensure HAL modules are loaded. transpile.ts warms the HAL registry once
148
+ // per transpile run (before the per-file IR build), so this is normally a
149
+ // cheap no-op. The non-forced call is a safety net for direct/test callers
150
+ // of buildProgramIR that didn't warm the registry first.
151
+ loadHALModules();
148
152
  const normalizedSourceText = normalizeEntrypointSyntax(sourceText);
149
153
  const source = parseSource(fileName, normalizedSourceText);
150
154
  const diagnostics = [];
@@ -439,14 +443,14 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
439
443
  }
440
444
  }
441
445
  }
442
- // Track HAL instances imported from board packages and framework stubs
443
- // so the HAL resolver can resolve them to Arduino C++ names.
446
+ // Track HAL instances imported from board packages and framework HAL
447
+ // subpaths so the HAL resolver can resolve them to framework C++ names.
444
448
  // The `@typecad/board` virtual import resolves to the board package, so it
445
449
  // also registers pin aliases (LED, etc.). Case-insensitive on the scope.
446
450
  const lowerSpecifier = moduleSpecifier.toLowerCase();
447
451
  const isHALSource = lowerSpecifier.startsWith('@typecad/board-')
448
- || lowerSpecifier === '@typecad/framework-arduino/arduino'
449
- || lowerSpecifier === '@typecad/board';
452
+ || lowerSpecifier === '@typecad/board'
453
+ || /^@typecad\/framework-[a-z0-9-]+\/(arduino|hal|gpio)$/.test(lowerSpecifier);
450
454
  // UI authoring namespace: `import { ui } from "@typecad/ui"`. The `ui`
451
455
  // value is a compile-time construct (its calls are intercepted by
452
456
  // tryResolveUICall); it must NOT be emitted as a C++ struct/value.
@@ -159,6 +159,22 @@ export function buildCallGraph(program) {
159
159
  }
160
160
  }
161
161
  }
162
+ // Registered callbacks from the HAL resolver (e.g. the arrow function passed
163
+ // to `Ble.characteristic(...).onRead(() => readTemp())`) are NOT present in
164
+ // top-level statements — they ride in `program.registeredCallbacks` and are
165
+ // later hoisted to file-scope `*_isr_*` functions by top-level-prep. The
166
+ // placeholder name (`__CALLBACK_N__`) is what surfaces in top-level deps,
167
+ // not the identifiers the callback body references (e.g. a free function
168
+ // like `readTemp`). Without this scan those references are invisible to the
169
+ // call graph: the function survives the hoist (its call sits inside the
170
+ // emitted ISR) but is tree-shaken as unreachable, and g++ later reports it
171
+ // "not declared in this scope". Attach their body identifiers to
172
+ // __top_level__ since the callbacks execute from the top-level entry path.
173
+ for (const rc of (program.registeredCallbacks ?? [])) {
174
+ for (const id of collectExpressionIdentifiers(rc.callbackIR)) {
175
+ topLevelDependencies.add(id);
176
+ }
177
+ }
162
178
  nodes.set("__top_level__", {
163
179
  name: "__top_level__",
164
180
  kind: "variable",