@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
@@ -0,0 +1,494 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Framework manifest validator
3
+ //
4
+ // Cross-checks each manifest claim against the loaded PlatformStrategy,
5
+ // Toolchain, and module exports. Errors are plain data (not thrown) so the
6
+ // validator can be called from tests, a future CLI, or agents reading errors
7
+ // programmatically.
8
+ //
9
+ // Categories A-H are documented in docs/framework-manifest-error-codes.md.
10
+ // ---------------------------------------------------------------------------
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { POLYFILL_BACKED_OPS } from './framework-manifest.js';
14
+ import { HAL_OPERATION_KINDS } from './hal-op-ir.js';
15
+ import { DISPLAY_OPERATION_KINDS } from './display-op-ir.js';
16
+ class Accumulator {
17
+ constructor() {
18
+ this.errors = [];
19
+ this.warnings = [];
20
+ }
21
+ error(code, path, message, subject) {
22
+ this.errors.push({ code, path, message, subject });
23
+ }
24
+ warning(code, path, message, subject) {
25
+ this.warnings.push({ code, path, message, subject });
26
+ }
27
+ }
28
+ // Map each HAL category to the prefix its op kinds share.
29
+ const CATEGORY_PREFIXES = {
30
+ gpio: ['gpio.'],
31
+ pwm: ['pwm.'],
32
+ rmt: ['rmt.'],
33
+ adc: ['adc.'],
34
+ dac: ['dac.'],
35
+ interrupts: ['interrupt.'],
36
+ tone: ['tone.'],
37
+ timing: ['timing.'],
38
+ power: ['power.'],
39
+ i2c: ['i2c.'],
40
+ spi: ['spi.'],
41
+ uart: ['uart.'],
42
+ pulse: ['pulse.'],
43
+ shift: ['shift.'],
44
+ board: ['board.'],
45
+ wdt: ['wdt.'],
46
+ wifi: ['wifi.'],
47
+ http: ['http.'],
48
+ display: ['display.'],
49
+ preferences: ['preferences.'],
50
+ ble: ['ble.'],
51
+ random: ['random.'],
52
+ fs: ['fs.'],
53
+ mdns: ['mdns.'],
54
+ mqtt: ['mqtt.'],
55
+ ota: ['ota.'],
56
+ temp: ['temp.'],
57
+ hwtimer: ['hwtimer.'],
58
+ capacitive: ['capacitive.'],
59
+ // Unimplemented-but-recognized categories: each framework may declare these
60
+ // as unsupported in its manifest so the coverage matrix records them as a
61
+ // roadmap. The validator probes the resolver and confirms it does NOT lower
62
+ // them (consistent with the 'unsupported' status).
63
+ i2s: ['i2s.'],
64
+ twai: ['twai.'],
65
+ usb: ['usb.'],
66
+ eth: ['eth.'],
67
+ espnow: ['espnow.'],
68
+ crypto: ['crypto.'],
69
+ pcnt: ['pcnt.'],
70
+ mcpwm: ['mcpwm.'],
71
+ // Worker offload + snprintf raw-escape: recognized categories. Frameworks
72
+ // that lower worker.* declare it supported; snprintf.emit is a raw escape
73
+ // hatch (declared unsupported by frameworks that don't special-case it).
74
+ worker: ['worker.'],
75
+ snprintf: ['snprintf.'],
76
+ };
77
+ function opKindsForCategory(category) {
78
+ const prefixes = CATEGORY_PREFIXES[category] ?? [];
79
+ const all = [...HAL_OPERATION_KINDS, ...DISPLAY_OPERATION_KINDS];
80
+ return all.filter((k) => prefixes.some((p) => k.startsWith(p)));
81
+ }
82
+ // Minimal valid payloads for op kinds whose resolver requires more than just
83
+ // the operation discriminator. The default probe (just `{ operation: kind }`)
84
+ // works for most ops; this map fills in the smallest payload that lets the
85
+ // resolver run without throwing on missing-arg validation.
86
+ //
87
+ // Templates must be the MINIMAL payload the resolver needs — enough to prove
88
+ // the framework handles the op, not to produce semantically correct output.
89
+ // Keep entries scoped to fields the resolver actually destructures.
90
+ const OP_PROBE_PAYLOADS = {
91
+ // i2c.write_bytes requires bus + bytes (resolver iterates bytes).
92
+ 'i2c.write_bytes': { bus: 'i2c0', address: 0x50, bytes: [0x00, 0x01] },
93
+ // uart.printf requires format + args (resolver joins args).
94
+ 'uart.printf': { port: 'Serial', format: '%d', args: ['x'] },
95
+ // adc.read / adc.read_voltage require pin.
96
+ 'adc.read': { pin: 0 },
97
+ 'adc.read_voltage': { pin: 0 },
98
+ // dac.write requires pin + value. Use pin 25 (a valid DAC pin on ESP32;
99
+ // pin 0 throws "not a DAC pin" on ESP32 and Arduino maps it to a
100
+ // non-DAC pin too).
101
+ 'dac.write': { pin: 25, value: 128 },
102
+ // display.* require coordinates/color (still inconclusive without driver
103
+ // context — included so future driver-aware probes can build on them).
104
+ 'display.fill_rect': { x: 0, y: 0, w: 10, h: 10, color: 0xffff },
105
+ 'display.draw_text': { x: 0, y: 0, text: 'x', font: '8x16', color: 0xffff },
106
+ 'display.draw_rect': { x: 0, y: 0, w: 10, h: 10, color: 0xffff },
107
+ 'display.flush': { rects: [{ x: 0, y: 0, w: 10, h: 10 }] },
108
+ // fs.* require a path (and content for write_text).
109
+ 'fs.read_text': { path: '/sdcard/x.txt' },
110
+ 'fs.write_text': { path: '/sdcard/x.txt', content: 'hi' },
111
+ 'fs.exists': { path: '/sdcard/x.txt' },
112
+ 'fs.remove': { path: '/sdcard/x.txt' },
113
+ // random.range requires min/max.
114
+ 'random.range': { min: 0, max: 10 },
115
+ // mdns.add_service requires instance/proto/port.
116
+ 'mdns.start': { hostname: 'h' },
117
+ 'mdns.set_hostname': { name: 'h' },
118
+ 'mdns.add_service': { instance: 'i', proto: '_tcp', port: 80 },
119
+ // mqtt.connect requires brokerUri/clientId; mqtt.publish requires topic/data.
120
+ 'mqtt.connect': { brokerUri: 'mqtt://b', clientId: 'c' },
121
+ 'mqtt.on_message': { handler: 'cb' },
122
+ 'mqtt.subscribe': { topic: 't' },
123
+ 'mqtt.publish': { topic: 't', data: 'd' },
124
+ // wifi.on_event requires an event + handler. Probe with 'connect'.
125
+ // Without a payload the probe sends event=undefined and a 'supported'
126
+ // declaration would false-fail as "resolver returned undefined".
127
+ 'wifi.on_event': { event: 'connect', handler: 'cb' },
128
+ // ota.from_url requires url; ota.write requires chunk.
129
+ 'ota.from_url': { url: 'https://x' },
130
+ 'ota.write': { chunk: 'buf' },
131
+ // hwtimer.* require an instance (and hz/handler).
132
+ 'hwtimer.set_frequency': { instance: 0, hz: 1000 },
133
+ 'hwtimer.on_overflow': { instance: 0, handler: 'cb' },
134
+ 'hwtimer.start': { instance: 0 },
135
+ 'hwtimer.stop': { instance: 0 },
136
+ // capacitive.read requires pin.
137
+ 'capacitive.read': { pin: 4 },
138
+ };
139
+ // Builds a HALOpIR probe. Uses OP_PROBE_PAYLOADS when available so resolvers
140
+ // that destructure required fields don't throw on the probe itself.
141
+ function buildHalProbe(opKind) {
142
+ const payload = OP_PROBE_PAYLOADS[opKind];
143
+ return { operation: opKind, ...(payload ?? {}) };
144
+ }
145
+ function buildDisplayProbe(opKind) {
146
+ const payload = OP_PROBE_PAYLOADS[opKind];
147
+ return { operation: opKind, ...(payload ?? {}) };
148
+ }
149
+ function probeResolve(strategy, opKind) {
150
+ const isDisplay = opKind.startsWith('display.');
151
+ try {
152
+ const op = isDisplay ? buildDisplayProbe(opKind) : buildHalProbe(opKind);
153
+ const result = isDisplay
154
+ ? strategy.resolveDisplayOp?.(op)
155
+ : strategy.resolveHALOperation?.(op);
156
+ if (!result)
157
+ return 'undefined';
158
+ if (result.code !== undefined)
159
+ return 'code';
160
+ if (result.expression !== undefined)
161
+ return 'expression';
162
+ return 'undefined';
163
+ }
164
+ catch {
165
+ return 'thrown';
166
+ }
167
+ }
168
+ function statusFromResolution(res) {
169
+ return res === 'undefined' || res === 'thrown' ? 'no-emit' : 'lowers';
170
+ }
171
+ // ---------------------------------------------------------------------------
172
+ // Category A — identity
173
+ // ---------------------------------------------------------------------------
174
+ function validateIdentity(manifest, ctx, acc) {
175
+ const strategyId = ctx.strategy.id;
176
+ const matchesDirect = strategyId === manifest.frameworkId;
177
+ const matchesInherited = manifest.inheritsStrategyId !== undefined &&
178
+ manifest.inheritsStrategyId === strategyId;
179
+ if (!matchesDirect && !matchesInherited) {
180
+ acc.error('identity/id-mismatch', 'frameworkId', `manifest.frameworkId is "${manifest.frameworkId}" but strategy.id is "${strategyId}". ` +
181
+ `Set strategy.id to match, or add "inheritsStrategyId: "${strategyId}"" to the manifest ` +
182
+ `to document intentional id reuse.`, manifest.frameworkId);
183
+ }
184
+ }
185
+ // ---------------------------------------------------------------------------
186
+ // Category B — entrypoint
187
+ // ---------------------------------------------------------------------------
188
+ function validateEntrypoint(manifest, ctx, acc) {
189
+ const ep = manifest.entrypoint;
190
+ const strat = ctx.strategy;
191
+ const checks = [
192
+ ['entrypointFunctionName', ep.entrypointFunctionName, strat.entrypointFunctionName()],
193
+ ['requiresLoopFunction', ep.requiresLoopFunction, strat.requiresLoopFunction()],
194
+ ['sourceExtension', ep.sourceExtension, strat.sourceExtension(true, false)],
195
+ ['generateHeaderFile', ep.generateHeaderFile, strat.generateHeaderFile()],
196
+ ];
197
+ for (const [field, declared, actual] of checks) {
198
+ if (declared !== actual) {
199
+ acc.error(`entrypoint/${field}/mismatch`, `entrypoint.${field}`, `manifest declares ${field}=${JSON.stringify(declared)} but strategy returns ${JSON.stringify(actual)}.`, field);
200
+ }
201
+ }
202
+ }
203
+ // ---------------------------------------------------------------------------
204
+ // Category C — HAL coverage
205
+ // ---------------------------------------------------------------------------
206
+ function validateHalCoverage(manifest, ctx, acc) {
207
+ for (const [category, declarationRaw] of Object.entries(manifest.hal)) {
208
+ if (category === 'raw')
209
+ continue;
210
+ const declaration = declarationRaw;
211
+ const knownKinds = opKindsForCategory(category);
212
+ // Check that every known op kind for this category appears in ops.
213
+ for (const kind of knownKinds) {
214
+ if (!(kind in declaration.ops)) {
215
+ acc.error(`hal/${category}/op/${kind}/undeclared`, `hal.${category}.ops.${kind}`, `manifest.hal.${category}.ops is missing "${kind}". Add it with a status of supported/stub/unsupported.`, kind);
216
+ }
217
+ }
218
+ // Probe each declared op and cross-check status vs resolver behavior.
219
+ for (const [kind, status] of Object.entries(declaration.ops)) {
220
+ // 'probe-inconclusive' is an explicit acknowledgment that the minimal
221
+ // probe (just an operation discriminator) cannot determine support —
222
+ // typically because the resolver needs a valid pin/config payload.
223
+ // The validator does not cross-check these; the renderer flags them
224
+ // for manual review. This keeps the matrix honest without penalizing
225
+ // frameworks for the probe's limitations.
226
+ if (status === 'probe-inconclusive')
227
+ continue;
228
+ // 'polyfill' means the op is backed by a runtime polyfill, NOT by
229
+ // resolveHALOperation. Verify the named polyfill exists in
230
+ // POLYFILL_BACKED_OPS and is declared in polyfills.emitted.
231
+ // No HAL probe — the resolver legitimately returns undefined for these.
232
+ if (status === 'polyfill') {
233
+ const expectedPolyfill = POLYFILL_BACKED_OPS[kind];
234
+ if (!expectedPolyfill) {
235
+ acc.error(`hal/${category}/op/${kind}/polyfill-not-recognized`, `hal.${category}.ops.${kind}`, `op "${kind}" declared "polyfill" but is not in POLYFILL_BACKED_OPS. Either add it to packages/cuttlefish/src/api/shared/framework-manifest.ts or use a different status.`, kind);
236
+ continue;
237
+ }
238
+ const emitted = manifest.polyfills.emitted.map((p) => p.id);
239
+ if (!emitted.includes(expectedPolyfill)) {
240
+ acc.error(`hal/${category}/op/${kind}/polyfill-not-declared`, `hal.${category}.ops.${kind}`, `op "${kind}" declared "polyfill" (backed by "${expectedPolyfill}") but "${expectedPolyfill}" is not in polyfills.emitted. Add it, or remove this op from the polyfill status.`, kind);
241
+ }
242
+ continue;
243
+ }
244
+ const res = probeResolve(ctx.strategy, kind);
245
+ const lowers = statusFromResolution(res) === 'lowers';
246
+ if (status === 'supported' && !lowers) {
247
+ acc.error(`hal/${category}/op/${kind}/status-mismatch`, `hal.${category}.ops.${kind}`, `op "${kind}" declared "supported" but resolver returned ${res}. Either implement the lowering or change the op status.`, kind);
248
+ }
249
+ else if (status === 'stub' && !lowers) {
250
+ acc.error(`hal/${category}/op/${kind}/status-mismatch`, `hal.${category}.ops.${kind}`, `op "${kind}" declared "stub" but resolver emitted nothing (${res}). Either emit something or change to "unsupported".`, kind);
251
+ }
252
+ else if (status === 'unsupported' && lowers) {
253
+ acc.error(`hal/${category}/op/${kind}/status-mismatch`, `hal.${category}.ops.${kind}`, `op "${kind}" declared "unsupported" but resolver actually lowers (${res}). Either mark it supported or remove the lowering.`, kind);
254
+ }
255
+ }
256
+ // Category-level summary check.
257
+ if (declaration.supported) {
258
+ // If every op is probe-inconclusive, we can't verify the supported
259
+ // claim at all — skip rather than false-positive.
260
+ const hasVerifiable = knownKinds.some((k) => declaration.ops[k] !== 'probe-inconclusive');
261
+ if (hasVerifiable) {
262
+ const anyLowersOrInconclusive = knownKinds.some((k) => {
263
+ if (declaration.ops[k] === 'probe-inconclusive')
264
+ return true;
265
+ if (declaration.ops[k] === 'polyfill')
266
+ return true;
267
+ return statusFromResolution(probeResolve(ctx.strategy, k)) === 'lowers';
268
+ });
269
+ if (!anyLowersOrInconclusive) {
270
+ acc.error(`hal/${category}/declared-supported-but-undefined`, `hal.${category}`, `manifest.hal.${category}.supported is true but resolver returns undefined for every verifiable op kind. Change supported to false with unsupportedReason, or implement the lowering.`, category);
271
+ }
272
+ }
273
+ }
274
+ else {
275
+ // supported: false. Ignore probe-inconclusive and polyfill ops when
276
+ // checking "does it actually lower?" — polyfill ops legitimately
277
+ // return undefined from the HAL resolver.
278
+ const anyLowers = knownKinds.some((k) => {
279
+ if (declaration.ops[k] === 'probe-inconclusive')
280
+ return false;
281
+ if (declaration.ops[k] === 'polyfill')
282
+ return false;
283
+ return statusFromResolution(probeResolve(ctx.strategy, k)) === 'lowers';
284
+ });
285
+ if (anyLowers) {
286
+ acc.error(`hal/${category}/declared-unsupported-but-actually-lowers`, `hal.${category}`, `manifest.hal.${category}.supported is false but resolver lowers at least one verifiable op. Either mark supported: true or override the resolver to throw/return undefined.`, category);
287
+ }
288
+ }
289
+ }
290
+ }
291
+ // ---------------------------------------------------------------------------
292
+ // Category C2 — HAL completeness
293
+ // ---------------------------------------------------------------------------
294
+ /**
295
+ * Verify every known HAL category appears in manifest.hal. validateHalCoverage
296
+ * only audits categories the manifest declares; a category omitted entirely is
297
+ * invisible to it (and to the per-op check). This step closes that hole: each
298
+ * category in CATEGORY_PREFIXES must be accounted for, even as unsupported.
299
+ * `raw` is excluded — it is the schema-level escape hatch, not a real category,
300
+ * and is not in CATEGORY_PREFIXES.
301
+ */
302
+ function validateHalCompleteness(manifest, _ctx, acc) {
303
+ const declared = new Set(Object.keys(manifest.hal));
304
+ for (const category of Object.keys(CATEGORY_PREFIXES)) {
305
+ if (!declared.has(category)) {
306
+ acc.error(`hal/${category}/category-undeclared`, `manifest.hal.${category}`, `manifest.hal is missing the "${category}" category. Add a block declaring it (supported: true/false with ops, or unsupported with unsupportedReason).`, category);
307
+ }
308
+ }
309
+ }
310
+ // ---------------------------------------------------------------------------
311
+ // Category D — polyfills
312
+ // ---------------------------------------------------------------------------
313
+ function validatePolyfills(manifest, ctx, acc) {
314
+ const strat = ctx.strategy;
315
+ // A polyfill is "produced" if either generateNativePolyfills OR
316
+ // nativePolyfills mentions it. generateNativePolyfills filters by program
317
+ // analysis (may omit polyfills not needed for the empty probe program);
318
+ // nativePolyfills is the unconditional set the strategy handles natively.
319
+ // Unioning both gives the true picture of what the strategy claims to emit.
320
+ const produced = new Set();
321
+ try {
322
+ const irs = strat.generateNativePolyfills?.({ kind: 'program', modules: [], classes: [], functions: [] }, undefined) ?? [];
323
+ for (const ir of irs)
324
+ produced.add(ir.id);
325
+ }
326
+ catch {
327
+ // generateNativePolyfills may throw on synthetic input; that's fine.
328
+ }
329
+ try {
330
+ const ids = strat.nativePolyfills?.() ?? new Set();
331
+ for (const id of ids)
332
+ produced.add(id);
333
+ }
334
+ catch {
335
+ // nativePolyfills is optional.
336
+ }
337
+ for (const declared of manifest.polyfills.emitted) {
338
+ if (!produced.has(declared.id)) {
339
+ acc.error(`polyfill/${declared.id}/declared-but-not-emitted`, `polyfills.emitted.${declared.id}`, `polyfill "${declared.id}" declared emitted but generateNativePolyfills/nativePolyfills did not produce it.`, declared.id);
340
+ }
341
+ }
342
+ for (const suppressed of manifest.polyfills.suppressed) {
343
+ if (produced.has(suppressed.id)) {
344
+ acc.error(`polyfill/${suppressed.id}/declared-suppressed-but-emitted`, `polyfills.suppressed.${suppressed.id}`, `polyfill "${suppressed.id}" declared suppressed but the strategy actually emits it.`, suppressed.id);
345
+ }
346
+ }
347
+ }
348
+ // ---------------------------------------------------------------------------
349
+ // Category E — toolchain
350
+ // ---------------------------------------------------------------------------
351
+ function validateToolchain(manifest, ctx, acc) {
352
+ const ops = manifest.toolchain.operations;
353
+ if (!ops.compile) {
354
+ acc.error('toolchain/compile/required', 'toolchain.operations.compile', 'toolchain.operations.compile must be true (LoadedFramework contract requires a compile implementation).', 'compile');
355
+ }
356
+ const tc = ctx.toolchain;
357
+ if (tc) {
358
+ const checks = [
359
+ ['prepare', ops.prepare],
360
+ ['compile', ops.compile],
361
+ ['upload', ops.upload],
362
+ ['monitor', ops.monitor],
363
+ ];
364
+ for (const [op, declared] of checks) {
365
+ if (declared && typeof tc[op] !== 'function') {
366
+ acc.error(`toolchain/${op}/declared-but-missing`, `toolchain.operations.${op}`, `operation "${op}" declared true but is not a function on the loaded Toolchain.`, op);
367
+ }
368
+ }
369
+ }
370
+ }
371
+ // ---------------------------------------------------------------------------
372
+ // Category F — library resolution
373
+ // ---------------------------------------------------------------------------
374
+ function validateLibraryResolution(manifest, ctx, acc) {
375
+ if (!manifest.libraryResolution)
376
+ return;
377
+ const lr = manifest.libraryResolution;
378
+ const checks = [
379
+ ['isFrameworkLibraryImport', lr.isFrameworkLibraryImport],
380
+ ['getFrameworkLibraryHeaderName', lr.getFrameworkLibraryHeaderName],
381
+ ['buildClassNameMap', lr.buildClassNameMap],
382
+ ['tryGenerateLibDecl', lr.tryGenerateLibDecl],
383
+ ];
384
+ for (const [name, declared] of checks) {
385
+ if (declared && typeof ctx.moduleExports[name] !== 'function') {
386
+ acc.error(`library-resolution/${name}/declared-but-not-exported`, `libraryResolution.${name}`, `"${name}" declared true but not exported from the framework package index.`, name);
387
+ }
388
+ }
389
+ }
390
+ // ---------------------------------------------------------------------------
391
+ // Category G — type emission
392
+ // ---------------------------------------------------------------------------
393
+ function validateTypeEmission(manifest, ctx, acc) {
394
+ const te = manifest.typeEmission;
395
+ const strat = ctx.strategy;
396
+ const fieldChecks = [
397
+ ['mathHeader', te.mathHeader, strat.mathHeader()],
398
+ ['needsStdString', te.needsStdString, strat.needsStdString()],
399
+ ['needsStdVector', te.needsStdVector, strat.needsStdVector()],
400
+ ['needsIostream', te.needsIostream, strat.needsIostream()],
401
+ ['needsStdFunction', te.needsStdFunction, strat.needsStdFunction()],
402
+ ];
403
+ for (const [field, declared, actual] of fieldChecks) {
404
+ if (declared !== actual) {
405
+ acc.error(`type-emission/${field}/mismatch`, `typeEmission.${field}`, `manifest declares ${field}=${JSON.stringify(declared)} but strategy returns ${JSON.stringify(actual)}.`, field);
406
+ }
407
+ }
408
+ const actualStdlib = strat.getStdLibSupport();
409
+ const declaredStdlib = te.stdlibSupport;
410
+ const stdlibEqual = actualStdlib.hasVector === declaredStdlib.hasVector &&
411
+ actualStdlib.hasString === declaredStdlib.hasString &&
412
+ actualStdlib.hasIostream === declaredStdlib.hasIostream &&
413
+ actualStdlib.hasExceptions === declaredStdlib.hasExceptions &&
414
+ actualStdlib.hasRTTI === declaredStdlib.hasRTTI &&
415
+ actualStdlib.recommendedArrayImpl === declaredStdlib.recommendedArrayImpl &&
416
+ actualStdlib.recommendedStringImpl === declaredStdlib.recommendedStringImpl;
417
+ if (!stdlibEqual) {
418
+ acc.error('type-emission/stdlib-support-mismatch', 'typeEmission.stdlibSupport', `manifest stdlibSupport does not match strategy.getStdLibSupport() return.`);
419
+ }
420
+ // Ambient types
421
+ const emittedAmbient = new Set();
422
+ try {
423
+ const decls = strat.ambientTypeDeclarations?.() ?? [];
424
+ for (const d of decls) {
425
+ if (typeof d === 'string') {
426
+ // Match all ambient declarations in the string: interface/type/class NAME
427
+ // and const NAME: (Arduino/AVR/ESP32 emit `const Timing: {...}` etc.).
428
+ const matches = [...d.matchAll(/\b(?:interface|type|class)\s+([A-Za-z_$][\w$]*)/g)];
429
+ for (const m of matches)
430
+ emittedAmbient.add(m[1]);
431
+ const constMatches = [...d.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*:/g)];
432
+ for (const m of constMatches)
433
+ emittedAmbient.add(m[1]);
434
+ }
435
+ else if (d && typeof d === 'object' && 'name' in d) {
436
+ emittedAmbient.add(String(d.name));
437
+ }
438
+ }
439
+ }
440
+ catch {
441
+ // ambientTypeDeclarations is optional and may require a context.
442
+ }
443
+ for (const declared of manifest.ambientTypes) {
444
+ if (!emittedAmbient.has(declared)) {
445
+ acc.error(`ambient-types/${declared}/declared-but-not-emitted`, `ambientTypes.${declared}`, `ambient type "${declared}" declared but not found in ambientTypeDeclarations() output.`, declared);
446
+ }
447
+ }
448
+ for (const emitted of emittedAmbient) {
449
+ if (!manifest.ambientTypes.includes(emitted)) {
450
+ acc.warning(`ambient-types/${emitted}/emitted-but-undeclared`, 'ambientTypes', `ambient type "${emitted}" emitted by strategy but not listed in manifest. Consider adding it.`, emitted);
451
+ }
452
+ }
453
+ }
454
+ // ---------------------------------------------------------------------------
455
+ // Category H — conformance
456
+ // ---------------------------------------------------------------------------
457
+ function validateConformance(manifest, ctx, acc) {
458
+ for (const group of manifest.conformance.hardwareTestGroups) {
459
+ const filePath = path.join(ctx.packageRoot, 'tests', `${group}.test.ts`);
460
+ if (!fs.existsSync(filePath)) {
461
+ acc.error(`conformance/hardware/${group}/file-not-found`, `conformance.hardwareTestGroups.${group}`, `hardware test group "${group}" listed but ${filePath} does not exist.`, group);
462
+ }
463
+ }
464
+ for (const name of manifest.conformance.halResolutionTests) {
465
+ // Test dir convention is tests/packages/<package-dir-name>/hal-resolution/.
466
+ // Package dir name is the last segment of packageName (e.g.
467
+ // "@typecad/framework-esp32" -> "framework-esp32"), not frameworkId.
468
+ const packageDirName = manifest.packageName.split('/').pop() ?? manifest.frameworkId;
469
+ const filePath = path.join(ctx.repoTestsDir, 'packages', packageDirName, 'hal-resolution', `${name}.test.ts`);
470
+ if (!fs.existsSync(filePath)) {
471
+ acc.error(`conformance/hal/${name}/file-not-found`, `conformance.halResolutionTests.${name}`, `HAL resolution test "${name}" listed but ${filePath} does not exist.`, name);
472
+ }
473
+ }
474
+ }
475
+ // ---------------------------------------------------------------------------
476
+ // Entry point
477
+ // ---------------------------------------------------------------------------
478
+ export function validateFrameworkManifest(manifest, ctx) {
479
+ const acc = new Accumulator();
480
+ validateIdentity(manifest, ctx, acc);
481
+ validateEntrypoint(manifest, ctx, acc);
482
+ validateHalCoverage(manifest, ctx, acc);
483
+ validateHalCompleteness(manifest, ctx, acc);
484
+ validatePolyfills(manifest, ctx, acc);
485
+ validateToolchain(manifest, ctx, acc);
486
+ validateLibraryResolution(manifest, ctx, acc);
487
+ validateTypeEmission(manifest, ctx, acc);
488
+ validateConformance(manifest, ctx, acc);
489
+ return {
490
+ valid: acc.errors.length === 0,
491
+ errors: acc.errors,
492
+ warnings: acc.warnings,
493
+ };
494
+ }
@@ -0,0 +1,22 @@
1
+ import type { ProgramIR } from "../index.js";
2
+ import type { RuntimePolyfillIR } from "./index.js";
3
+ import type { PlatformStrategy } from "./platform-strategy.js";
4
+ import type { WorkerBacking } from "./worker-runtime.js";
5
+ /**
6
+ * Build a RuntimePolyfillIR for the worker-offload runtime, when the framework
7
+ * supplies worker backing via its PlatformAsyncStrategy hooks
8
+ * (workerSpawnLines / workerSignalDoneExpr / workerIsDoneExpr).
9
+ *
10
+ * The polyfill carries only the slot table + __tc_worker_submit/done contract
11
+ * with the per-framework primitives injected from the strategy hooks. STL-free
12
+ * so it links on minimal-libc targets (Zephyr). Returns null if the strategy
13
+ * supplies no backing (worker offload unsupported there) — in which case
14
+ * worker.* ops resolve to unsupported.
15
+ *
16
+ * Callers gate this on the program actually using worker.* ops
17
+ * (ProgramAnalysisResult.usesWorker); the producer does not re-scan.
18
+ */
19
+ export declare function buildWorkerRuntimePolyfill(_program: ProgramIR, strategy: PlatformStrategy, backing: WorkerBacking, options?: {
20
+ poolSize?: number;
21
+ stackSize?: number;
22
+ }): RuntimePolyfillIR | null;
@@ -0,0 +1,34 @@
1
+ import { generateWorkerRuntime } from "./worker-runtime.js";
2
+ /**
3
+ * Build a RuntimePolyfillIR for the worker-offload runtime, when the framework
4
+ * supplies worker backing via its PlatformAsyncStrategy hooks
5
+ * (workerSpawnLines / workerSignalDoneExpr / workerIsDoneExpr).
6
+ *
7
+ * The polyfill carries only the slot table + __tc_worker_submit/done contract
8
+ * with the per-framework primitives injected from the strategy hooks. STL-free
9
+ * so it links on minimal-libc targets (Zephyr). Returns null if the strategy
10
+ * supplies no backing (worker offload unsupported there) — in which case
11
+ * worker.* ops resolve to unsupported.
12
+ *
13
+ * Callers gate this on the program actually using worker.* ops
14
+ * (ProgramAnalysisResult.usesWorker); the producer does not re-scan.
15
+ */
16
+ export function buildWorkerRuntimePolyfill(_program, strategy, backing, options = {}) {
17
+ // The backing is authoritative: it is constructed by the framework from its
18
+ // own hooks. If a framework supplies no backing (worker offload unsupported),
19
+ // it should not call this producer.
20
+ const runtime = generateWorkerRuntime(backing, options);
21
+ if (!runtime)
22
+ return null;
23
+ return {
24
+ kind: "polyfill",
25
+ id: "worker_runtime",
26
+ domain: "embedded",
27
+ requiredIncludes: backing.requiredIncludes,
28
+ forwardDeclarations: backing.declarations ?? [],
29
+ helperStructs: [runtime],
30
+ helperFunctions: [],
31
+ shimMacros: [],
32
+ dependencies: [],
33
+ };
34
+ }
@@ -0,0 +1,69 @@
1
+ export interface WorkerRuntimeOptions {
2
+ /** Number of fixed worker slots in the pool. Default 4. */
3
+ poolSize?: number;
4
+ /** Per-slot worker stack size in bytes (ESP32). Default 8192. */
5
+ stackSize?: number;
6
+ }
7
+ /**
8
+ * Per-framework backing primitives injected into the shared contract.
9
+ * Undefined fields mean "unsupported" and the runtime is not emitted.
10
+ */
11
+ export interface WorkerBacking {
12
+ /**
13
+ * Statements that spawn the worker trampoline for slot `handleId`.
14
+ *
15
+ * `trampolineName` is the C++ symbol of the per-slot worker function the
16
+ * shared runtime generated (it reads fn/arg from the slot, runs them, then
17
+ * signals completion). `waiterExpr` is the C++ lvalue the backing should set
18
+ * to the current waiter handle before spawning (the shared runtime reads it
19
+ * via signalDoneExpr/isDoneExpr).
20
+ *
21
+ * The shared runtime has already stored the submitted fn/arg into the slot
22
+ * and cleared `done` before this runs; the backing need only record the
23
+ * waiter and spawn `trampolineName`.
24
+ */
25
+ spawnLines: (handleId: number, trampolineName: string, waiterExpr: string) => string[];
26
+ /** Expression the worker calls after finishing + writing outputs (barrier). */
27
+ signalDoneExpr: (handleId: number) => string;
28
+ /** Boolean expression polled by __tc_worker_done (observes the barrier). */
29
+ isDoneExpr: (handleId: number) => string;
30
+ /** C++ headers required by the primitives (e.g. '<freertos/FreeRTOS.h>'). */
31
+ requiredIncludes: string[];
32
+ /** Any backing declarations (e.g. semaphore objects) emitted before the pool. */
33
+ declarations?: string[];
34
+ /**
35
+ * Per-slot backing declarations emitted inside the namespace, one call per
36
+ * slot, before the submit/done functions. Used by backings that need a named
37
+ * per-slot object bound to the trampoline at definition time — e.g. Zephyr's
38
+ * `K_WORK_DEFINE(_work_<i>, __tc_worker_fn_<i>)` + a per-slot semaphore.
39
+ * `trampolineName` is the per-slot worker symbol the shared runtime emitted.
40
+ */
41
+ slotDeclarations?: (handleId: number, trampolineName: string) => string[];
42
+ }
43
+ /**
44
+ * Generate the worker-offload runtime C++ for a framework that supplies a
45
+ * backing. Returns null if `backing` is null (unsupported on this framework).
46
+ *
47
+ * The emitted code defines, for each slot i in [0, poolSize):
48
+ * - a worker trampoline __tc_worker_fn_i(void* arg) that runs the recorded
49
+ * fn and then signals completion via signalDoneExpr,
50
+ * - __tc_worker_submit(i, fn, arg) — records the waiter, clears done, spawns,
51
+ * - __tc_worker_done(i) — poll predicate.
52
+ *
53
+ * AUTOSAR C++14: fixed-width types, static_cast, final structs, no STL, no heap.
54
+ */
55
+ export declare function generateWorkerRuntime(backing: WorkerBacking | null, options?: WorkerRuntimeOptions): string | null;
56
+ /**
57
+ * Resolve a HAL worker.* op to C++ against the shared contract.
58
+ * Returns undefined for ops the caller did not lower (unsupported).
59
+ *
60
+ * Note: the worker function itself (fnRef) and its argument (argRef) come from
61
+ * the IR as flat-scalar strings (per the hal-op-ir flat-fields contract). The
62
+ * lowering emits a straight call into __tc_worker_submit / __tc_worker_done.
63
+ */
64
+ export declare function lowerWorkerOp(op: {
65
+ operation: string;
66
+ }): {
67
+ code?: string;
68
+ expression?: string;
69
+ } | undefined;