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

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 (90) hide show
  1. package/dist/api/shared/display-adapter.d.ts +2 -1
  2. package/dist/api/shared/display-adapter.js +8 -1
  3. package/dist/api/shared/display-adapters/sdl.js +9 -2
  4. package/dist/api/shared/display-profile.d.ts +20 -3
  5. package/dist/api/shared/display-profile.js +21 -6
  6. package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
  7. package/dist/api/shared/framework-manifest-registry.js +25 -0
  8. package/dist/api/shared/framework-manifest.d.ts +462 -0
  9. package/dist/api/shared/framework-manifest.js +149 -0
  10. package/dist/api/shared/glcdfont.d.ts +12 -0
  11. package/dist/api/shared/glcdfont.js +124 -0
  12. package/dist/api/shared/graphics-strategy.d.ts +28 -0
  13. package/dist/api/shared/hal-op-ir.d.ts +418 -1
  14. package/dist/api/shared/hal-op-ir.js +95 -1
  15. package/dist/api/shared/index.d.ts +11 -1
  16. package/dist/api/shared/index.js +14 -0
  17. package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
  18. package/dist/api/shared/native-display-op-resolver.js +64 -0
  19. package/dist/api/shared/platform-strategy.d.ts +9 -0
  20. package/dist/api/shared/promise-runtime.js +78 -0
  21. package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
  22. package/dist/api/shared/validate-framework-manifest.js +417 -0
  23. package/dist/cli.js +109 -4
  24. package/dist/config-loader.js +20 -1
  25. package/dist/config-schema.d.ts +36 -36
  26. package/dist/create/board-spec.d.ts +4 -4
  27. package/dist/create/init-scaffold.d.ts +3 -0
  28. package/dist/create/init-scaffold.js +48 -0
  29. package/dist/create/init-templates.d.ts +2 -0
  30. package/dist/create/init-templates.js +28 -8
  31. package/dist/create/init-wizard.js +20 -1
  32. package/dist/emit/cpp-emitter.js +4 -3
  33. package/dist/emit/emitters/emitter-context.d.ts +5 -0
  34. package/dist/emit/emitters/function-emitter-impl.js +69 -59
  35. package/dist/emit/emitters/output-finalizer.d.ts +6 -0
  36. package/dist/emit/emitters/output-finalizer.js +21 -11
  37. package/dist/emit/emitters/setup.js +40 -0
  38. package/dist/emit/emitters/top-level-prep.js +2 -0
  39. package/dist/emit/emitters/ui-emitter.js +23 -8
  40. package/dist/emit/expression-renderer.js +10 -1
  41. package/dist/emit/snprintf-helpers.js +8 -0
  42. package/dist/emit/statement-renderer.js +13 -0
  43. package/dist/emit/utils/async-state-machine.js +185 -116
  44. package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
  45. package/dist/emit/utils/hal-op-cpp-type.js +40 -0
  46. package/dist/ir/build-ir.js +5 -1
  47. package/dist/ir/expression-to-ir.js +14 -0
  48. package/dist/ir/feature-registry.js +7 -25
  49. package/dist/ir/hal/hal-emitter.d.ts +5 -2
  50. package/dist/ir/hal/hal-emitter.js +40 -12
  51. package/dist/ir/hal/hal-parser.d.ts +6 -0
  52. package/dist/ir/hal/hal-parser.js +74 -0
  53. package/dist/ir/hal/hal-plugins.js +561 -0
  54. package/dist/ir/identifier-collector.js +18 -0
  55. package/dist/ir/network-validation.d.ts +4 -0
  56. package/dist/ir/network-validation.js +184 -0
  57. package/dist/ir/ownership-analysis.js +14 -1
  58. package/dist/ir/program-analysis.d.ts +23 -0
  59. package/dist/ir/program-analysis.js +81 -2
  60. package/dist/ir/timing-validation.d.ts +6 -1
  61. package/dist/ir/timing-validation.js +50 -12
  62. package/dist/ir/transformers/expressions.js +58 -0
  63. package/dist/ir/transformers/hal-emit-helpers.js +1 -1
  64. package/dist/ir/transformers/variables.js +41 -3
  65. package/dist/ir/validation-orchestrator.js +3 -1
  66. package/dist/libdef/c-to-decl.d.ts +27 -0
  67. package/dist/libdef/c-to-decl.js +397 -0
  68. package/dist/libdef/component-decls.d.ts +2 -0
  69. package/dist/libdef/component-decls.js +6 -0
  70. package/dist/libdef/component-discovery.d.ts +43 -0
  71. package/dist/libdef/component-discovery.js +83 -0
  72. package/dist/libdef/cpp-to-decl.d.ts +9 -0
  73. package/dist/libdef/cpp-to-decl.js +72 -0
  74. package/dist/libdef/idf-discovery.d.ts +7 -0
  75. package/dist/libdef/idf-discovery.js +59 -0
  76. package/dist/libdef/registry.js +5 -2
  77. package/dist/lint-cache.d.ts +59 -0
  78. package/dist/lint-cache.js +257 -0
  79. package/dist/orchestrator/graph-builder.js +6 -2
  80. package/dist/stores/display-profile-store.d.ts +1 -0
  81. package/dist/stores/display-profile-store.js +1 -0
  82. package/dist/testing.d.ts +3 -1
  83. package/dist/testing.js +3 -1
  84. package/dist/transpile.d.ts +3 -0
  85. package/dist/transpile.js +72 -27
  86. package/dist/ui-hook.d.ts +17 -1
  87. package/dist/utils/cli.js +27 -1
  88. package/dist/utils/fs.d.ts +13 -0
  89. package/dist/utils/fs.js +50 -0
  90. package/package.json +8 -4
@@ -128,7 +128,12 @@ export function emitCallbackFunctions(ctx) {
128
128
  appendSourceLine(ctx, `const unsigned long ${callback.name}_debounce = ${callback.debounceMs};`);
129
129
  appendSourceLine(ctx, "");
130
130
  }
131
- appendSourceLine(ctx, `${strategy.isrFunctionAttribute?.() ?? ""}${renderCallbackSignature(callback)} {`);
131
+ // IRAM_ATTR only on true ISR definitions. ESP-IDF's IRAM_ATTR uses
132
+ // __COUNTER__, so putting it on both forward decl and definition assigns
133
+ // conflicting .iram1.N sections (-Werror=attributes). Non-ISR callbacks
134
+ // (WiFi events, timers) must not be IRAM-placed either — they call printf.
135
+ const isrAttr = callback.isInterruptHandler ? (strategy.isrFunctionAttribute?.() ?? "") : "";
136
+ appendSourceLine(ctx, `${isrAttr}${renderCallbackSignature(callback)} {`);
132
137
  if (callback.debounceMs !== undefined && callback.debounceMs > 0) {
133
138
  appendSourceLine(ctx, ` volatile unsigned long now = ${strategy.currentTimeMillis()};`);
134
139
  appendSourceLine(ctx, ` if (now - ${callback.name}_lastTime < ${callback.name}_debounce) return;`);
@@ -141,10 +146,11 @@ export function emitCallbackFunctions(ctx) {
141
146
  appendSourceLine(ctx, "}");
142
147
  appendSourceLine(ctx, "");
143
148
  }
144
- // Split-mode ISR callback forward declarations in header
149
+ // Split-mode ISR callback forward declarations in header (no IRAM_ATTR —
150
+ // attribute belongs on the definition only; see comment above).
145
151
  if (effectiveEmitMode === "split") {
146
152
  for (const callback of ctx.callbackFunctions) {
147
- appendHeaderLine(ctx, `${strategy.isrFunctionAttribute?.() ?? ""}${renderCallbackSignature(callback)};`);
153
+ appendHeaderLine(ctx, `${renderCallbackSignature(callback)};`);
148
154
  }
149
155
  }
150
156
  }
@@ -172,6 +178,10 @@ export function emitFunctions(ctx) {
172
178
  // exported functions (which ride along with their definition below).
173
179
  for (let fi = 0; fi < mappedFunctions.length; fi++) {
174
180
  const fn = mappedFunctions[fi];
181
+ // Async functions become cooperative *Task state machines (emitAsyncTaskClasses).
182
+ // Emitting an empty stub here triggers -Wunused-function with no value.
183
+ if (fn.isAsync && ctx.hasAsyncRuntime)
184
+ continue;
175
185
  const declarationParameterList = ctx.statementRenderer.renderParameters(fn.parameters, true);
176
186
  const definitionParameterList = ctx.statementRenderer.renderParameters(fn.parameters, false);
177
187
  const readonlyPrefix = fn.isReadonlyReturnType ? "const " : "";
@@ -246,65 +256,60 @@ export function emitFunctions(ctx) {
246
256
  if (hasPromiseRuntime && fn.name === asyncDriverFn) {
247
257
  appendSourceLine(ctx, " cuttlefish_pump_microtasks();");
248
258
  }
249
- if (fn.isAsync && ctx.hasAsyncRuntime) {
250
- appendSourceLine(ctx, ` // driven as cooperative task in ${asyncDriverFn}()`);
251
- }
252
- else {
253
- if (fn.typeParameterConstraints) {
254
- for (const [param, expr] of fn.typeParameterConstraints) {
255
- appendSourceLine(ctx, ` static_assert(${expr}, "${param} constraint violated");`);
256
- }
259
+ if (fn.typeParameterConstraints) {
260
+ for (const [param, expr] of fn.typeParameterConstraints) {
261
+ appendSourceLine(ctx, ` static_assert(${expr}, "${param} constraint violated");`);
257
262
  }
258
- const functionScope = createChildEmissionScope(topLevelScope, fn.parameters);
259
- ctx.cArrayVarNames = ctx.fnCArrayVarNames.get(String(fi)) ?? new Set();
260
- const isLoopDriver = fn.name === asyncDriverFn;
261
- const lastStmt = fn.statements.length > 0 ? fn.statements[fn.statements.length - 1] : null;
262
- const lastIsReturn = lastStmt?.kind === "return";
263
- const stmtsToEmit = (isLoopDriver && lastIsReturn)
264
- ? fn.statements.slice(0, -1)
265
- : fn.statements;
266
- for (const statement of stmtsToEmit) {
267
- appendRenderedStatement(ctx, statement, " ", functionScope);
268
- }
269
- // Drive the UI runtime each frame. Fires only in the driver function when
270
- // a UI is mounted (entryHasUI). Uses a separate gate from the async pump
271
- // so a pure-UI program (no async/timers) still animates. When the strategy
272
- // provides hostEventLoop, the per-frame work runs inside a while loop so a
273
- // host target (SDL) can pump events and present between ticks. Emitted
274
- // AFTER the function's init statements (ui_init/display_init/touch_init)
275
- // so initialization precedes the loop.
276
- if (entryHasUI() && fn.name === asyncDriverFn) {
277
- const loop = strategy.hostEventLoop?.();
278
- if (loop) {
279
- appendSourceLine(ctx, ` bool ${loop.flagName} = true;`);
280
- appendSourceLine(ctx, ` while (${loop.continueCondition}) {`);
281
- appendSourceLine(ctx, ` ${loop.preIteration}`);
282
- }
283
- appendSourceLine(ctx, ` uint32_t __tc_ui_now = (uint32_t)${strategy.currentTimeMillis()};`);
284
- appendSourceLine(ctx, " static uint32_t __tc_ui_last_tick = __tc_ui_now;");
285
- appendSourceLine(ctx, " uint32_t __tc_ui_delta = __tc_ui_now - __tc_ui_last_tick;");
286
- appendSourceLine(ctx, " __tc_ui_last_tick = __tc_ui_now;");
287
- appendSourceLine(ctx, " if (__tc_ui_delta > 250) __tc_ui_delta = 250;");
288
- appendSourceLine(ctx, " ui_tick((uint16_t)__tc_ui_delta);");
289
- if (loop) {
290
- appendSourceLine(ctx, ` ${loop.postIteration}`);
291
- appendSourceLine(ctx, ` }`);
292
- }
263
+ }
264
+ const functionScope = createChildEmissionScope(topLevelScope, fn.parameters);
265
+ ctx.cArrayVarNames = ctx.fnCArrayVarNames.get(String(fi)) ?? new Set();
266
+ const isLoopDriver = fn.name === asyncDriverFn;
267
+ const lastStmt = fn.statements.length > 0 ? fn.statements[fn.statements.length - 1] : null;
268
+ const lastIsReturn = lastStmt?.kind === "return";
269
+ const stmtsToEmit = (isLoopDriver && lastIsReturn)
270
+ ? fn.statements.slice(0, -1)
271
+ : fn.statements;
272
+ for (const statement of stmtsToEmit) {
273
+ appendRenderedStatement(ctx, statement, " ", functionScope);
274
+ }
275
+ // Drive the UI runtime each frame. Fires only in the driver function when
276
+ // a UI is mounted (entryHasUI). Uses a separate gate from the async pump
277
+ // so a pure-UI program (no async/timers) still animates. When the strategy
278
+ // provides hostEventLoop, the per-frame work runs inside a while loop so a
279
+ // host target (SDL) can pump events and present between ticks. Emitted
280
+ // AFTER the function's init statements (ui_init/display_init/touch_init)
281
+ // so initialization precedes the loop.
282
+ if (entryHasUI() && fn.name === asyncDriverFn) {
283
+ const loop = strategy.hostEventLoop?.();
284
+ if (loop) {
285
+ appendSourceLine(ctx, ` bool ${loop.flagName} = true;`);
286
+ appendSourceLine(ctx, ` while (${loop.continueCondition}) {`);
287
+ appendSourceLine(ctx, ` ${loop.preIteration}`);
293
288
  }
294
- if (isLoopDriver && (hasPromiseRuntime || asyncTaskClasses.length > 0 || usesTimers)) {
295
- const taskNames = asyncTaskClasses.map(t => t.taskVarName);
296
- const asyncConfig = strategy.getAsyncRuntimeConfig();
297
- asyncConfig.hasPromiseRuntime = hasPromiseRuntime;
298
- asyncConfig.hasTimers = usesTimers;
299
- const injectionLines = strategy.asyncLoopInjection(taskNames, asyncConfig);
300
- for (const line of injectionLines) {
301
- appendSourceLine(ctx, ` ${line}`);
302
- }
289
+ appendSourceLine(ctx, ` uint32_t __tc_ui_now = (uint32_t)${strategy.currentTimeMillis()};`);
290
+ appendSourceLine(ctx, " static uint32_t __tc_ui_last_tick = __tc_ui_now;");
291
+ appendSourceLine(ctx, " uint32_t __tc_ui_delta = __tc_ui_now - __tc_ui_last_tick;");
292
+ appendSourceLine(ctx, " __tc_ui_last_tick = __tc_ui_now;");
293
+ appendSourceLine(ctx, " if (__tc_ui_delta > 250) __tc_ui_delta = 250;");
294
+ appendSourceLine(ctx, " ui_tick((uint16_t)__tc_ui_delta);");
295
+ if (loop) {
296
+ appendSourceLine(ctx, ` ${loop.postIteration}`);
297
+ appendSourceLine(ctx, ` }`);
303
298
  }
304
- if (isLoopDriver && lastIsReturn) {
305
- appendRenderedStatement(ctx, lastStmt, " ", functionScope);
299
+ }
300
+ if (isLoopDriver && (hasPromiseRuntime || asyncTaskClasses.length > 0 || usesTimers)) {
301
+ const taskNames = asyncTaskClasses.map(t => t.taskVarName);
302
+ const asyncConfig = strategy.getAsyncRuntimeConfig();
303
+ asyncConfig.hasPromiseRuntime = hasPromiseRuntime;
304
+ asyncConfig.hasTimers = usesTimers;
305
+ const injectionLines = strategy.asyncLoopInjection(taskNames, asyncConfig);
306
+ for (const line of injectionLines) {
307
+ appendSourceLine(ctx, ` ${line}`);
306
308
  }
307
309
  }
310
+ if (isLoopDriver && lastIsReturn) {
311
+ appendRenderedStatement(ctx, lastStmt, " ", functionScope);
312
+ }
308
313
  appendSourceLine(ctx, "}");
309
314
  emitCommentLines(fn.trailingComments, "", (line) => appendSourceLine(ctx, line));
310
315
  appendSourceLine(ctx, "");
@@ -338,16 +343,21 @@ export function emitFunctions(ctx) {
338
343
  export function emitFunctionForwardDeclarations(ctx) {
339
344
  const { strategy, effectiveEmitMode } = ctx;
340
345
  const excludedNames = new Set(strategy.forwardDeclarationExclusions?.() ?? []);
341
- // ISR callback forward declarations.
346
+ // Callback forward declarations (no IRAM_ATTR — ESP-IDF's attribute uses
347
+ // __COUNTER__, so decl+def would get conflicting .iram1.N sections).
342
348
  if (effectiveEmitMode !== "split") {
343
349
  for (const callback of ctx.callbackFunctions) {
344
- appendSourceLine(ctx, `${strategy.isrFunctionAttribute?.() ?? ""}${renderCallbackSignature(callback)};`);
350
+ appendSourceLine(ctx, `${renderCallbackSignature(callback)};`);
345
351
  }
346
352
  }
347
353
  let emittedAnyFn = false;
348
354
  for (const fn of ctx.mappedFunctions) {
349
355
  if (excludedNames.has(fn.name))
350
356
  continue;
357
+ // Async functions become *Task classes; calls lower to `fooTask.run()`.
358
+ // Forward-declaring empty stubs triggers -Wunused-function.
359
+ if (fn.isAsync && ctx.hasAsyncRuntime)
360
+ continue;
351
361
  const isExported = fn.isExported === true;
352
362
  // A function is an entrypoint if it is the strategy's primary entrypoint
353
363
  // (e.g. `setup` on Arduino, `main` on native) OR one of the platform's
@@ -1,4 +1,10 @@
1
1
  import type { GeneratedOutputs } from "../../types.js";
2
2
  import type { EmitterContext } from "./emitter-context.js";
3
3
  export declare function emitPreamble(ctx: EmitterContext): void;
4
+ /**
5
+ * Emit cooperative async state-machine classes. Must run AFTER top-level
6
+ * globals (emitTypeDeclarations) so identifiers like WIFI_SSID referenced
7
+ * from task bodies are already declared.
8
+ */
9
+ export declare function emitAsyncTaskClasses(ctx: EmitterContext): void;
4
10
  export declare function finalizeOutput(ctx: EmitterContext): GeneratedOutputs;
@@ -26,7 +26,7 @@ function polyfillDefinitionGuard(definition) {
26
26
  return null;
27
27
  }
28
28
  export function emitPreamble(ctx) {
29
- const { strategy, effectiveEmitMode, program, shimLines, emittedPolyfills, asyncTaskClasses, includes, programAnalysis } = ctx;
29
+ const { strategy, effectiveEmitMode, program, shimLines, emittedPolyfills, includes, programAnalysis } = ctx;
30
30
  for (const include of dedupe(includes)) {
31
31
  appendSourceLineLocal(ctx, `#include ${include}`);
32
32
  }
@@ -73,16 +73,8 @@ export function emitPreamble(ctx) {
73
73
  }
74
74
  appendSourceLineLocal(ctx, "");
75
75
  }
76
- if (asyncTaskClasses.length > 0) {
77
- for (const { classDef, instanceDecl } of asyncTaskClasses) {
78
- for (const line of classDef.split("\n")) {
79
- appendSourceLineLocal(ctx, line);
80
- }
81
- appendSourceLineLocal(ctx, "");
82
- appendSourceLineLocal(ctx, instanceDecl);
83
- appendSourceLineLocal(ctx, "");
84
- }
85
- }
76
+ // Async task classes are emitted later (emitAsyncTaskClasses) AFTER top-level
77
+ // globals so references like WIFI_SSID inside the state machine compile.
86
78
  if (strategy.needsVectorOverload() && hasConsoleCalls(program, strategy) && (programAnalysis.usesVectorTypes || programAnalysis.hasArrayInObjectLiteral)) {
87
79
  appendSourceLineLocal(ctx, "template <typename T>");
88
80
  appendSourceLineLocal(ctx, "std::ostream& operator<<(std::ostream& os, const std::vector<T>& values)");
@@ -136,6 +128,24 @@ export function emitPreamble(ctx) {
136
128
  appendSourceLineLocal(ctx, "");
137
129
  }
138
130
  }
131
+ /**
132
+ * Emit cooperative async state-machine classes. Must run AFTER top-level
133
+ * globals (emitTypeDeclarations) so identifiers like WIFI_SSID referenced
134
+ * from task bodies are already declared.
135
+ */
136
+ export function emitAsyncTaskClasses(ctx) {
137
+ const { asyncTaskClasses } = ctx;
138
+ if (asyncTaskClasses.length === 0)
139
+ return;
140
+ for (const { classDef, instanceDecl } of asyncTaskClasses) {
141
+ for (const line of classDef.split("\n")) {
142
+ appendSourceLineLocal(ctx, line);
143
+ }
144
+ appendSourceLineLocal(ctx, "");
145
+ appendSourceLineLocal(ctx, instanceDecl);
146
+ appendSourceLineLocal(ctx, "");
147
+ }
148
+ }
139
149
  export function finalizeOutput(ctx) {
140
150
  const { program, strategy, options, effectiveEmitMode, isNpmPackage, baseName, includes, shimLines, emittedPolyfills } = ctx;
141
151
  const outDir = options.outDir;
@@ -327,6 +327,46 @@ export function buildEmitterContext(program, options) {
327
327
  if (!programAnalysis.usesTone) {
328
328
  shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_TONE_BEGIN', '// CUTTLEFISH_TONE_END');
329
329
  }
330
+ // framework-esp32 native peripheral driver shims. Same defensive-backstop
331
+ // pattern as the UART/SPI/TWI/EEPROM/tone blocks above. framework-arduino's
332
+ // and framework-avr's shimLines contain none of these markers, so these
333
+ // filters are no-ops there.
334
+ if (!programAnalysis.usesGPIO) {
335
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_GPIO_BEGIN', '// CUTTLEFISH_GPIO_END');
336
+ }
337
+ if (!programAnalysis.usesPWM) {
338
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_PWM_BEGIN', '// CUTTLEFISH_PWM_END');
339
+ }
340
+ if (!programAnalysis.usesADC) {
341
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_ADC_BEGIN', '// CUTTLEFISH_ADC_END');
342
+ }
343
+ if (!programAnalysis.usesDAC) {
344
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_DAC_BEGIN', '// CUTTLEFISH_DAC_END');
345
+ }
346
+ if (!programAnalysis.usesPower) {
347
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_POWER_BEGIN', '// CUTTLEFISH_POWER_END');
348
+ }
349
+ if (!programAnalysis.usesWdt) {
350
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_WDT_BEGIN', '// CUTTLEFISH_WDT_END');
351
+ }
352
+ if (!programAnalysis.usesInterrupts) {
353
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_INTR_BEGIN', '// CUTTLEFISH_INTR_END');
354
+ }
355
+ if (!programAnalysis.usesPulse) {
356
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_PULSE_BEGIN', '// CUTTLEFISH_PULSE_END');
357
+ }
358
+ if (!programAnalysis.usesShift) {
359
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_SHIFT_BEGIN', '// CUTTLEFISH_SHIFT_END');
360
+ }
361
+ if (!programAnalysis.usesWifi) {
362
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_WIFI_BEGIN', '// CUTTLEFISH_WIFI_END');
363
+ }
364
+ if (!programAnalysis.usesHttp) {
365
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_HTTP_BEGIN', '// CUTTLEFISH_HTTP_END');
366
+ }
367
+ if (!programAnalysis.usesPreferences) {
368
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_PREFERENCES_BEGIN', '// CUTTLEFISH_PREFERENCES_END');
369
+ }
330
370
  profileDiagnostics = [...strategy.profileDiagnostics(program, options.platformContext)];
331
371
  }
332
372
  for (const imported of program.imports) {
@@ -597,6 +597,8 @@ export function runTopLevelPreprocessing(ctx) {
597
597
  statements: callbackIR.statements ?? callbackIR.body ?? [],
598
598
  debounceMs: callbackIR.debounceMs,
599
599
  isInterruptHandler: callbackIR.isInterruptHandler,
600
+ ...(callbackIR.returnType ? { returnType: callbackIR.returnType } : {}),
601
+ ...(callbackIR.typedParams ? { typedParams: callbackIR.typedParams } : {}),
600
602
  });
601
603
  replacePlaceholderInAllStatements(filteredTopLevelExecutables, rc.placeholderName, callbackName);
602
604
  for (const fn of mappedFunctions) {
@@ -34,7 +34,7 @@ import { getRadioGroups } from "../../ir/ui-element-auto-wire.js";
34
34
  // minPressure gate is skipped for them. Resistive controllers (XPT2046,
35
35
  // STMPE610, Adafruit_TouchScreen) keep the gate.
36
36
  // ---------------------------------------------------------------------------
37
- const CAPACITIVE_TOUCH_LIBS = new Set(["FT6336U", "sdl"]);
37
+ const CAPACITIVE_TOUCH_LIBS = new Set(["FT6336U", "GT911", "CST816S", "sdl"]);
38
38
  /** Generate the ui_poll_touch() body, or null for the no-touch path. */
39
39
  export function generateTouchPollBody(input) {
40
40
  const { library, minPressure, calibration, profile } = input;
@@ -141,9 +141,14 @@ export function emitUIRuntime(ctx) {
141
141
  // display-adapter registry. Must precede the runtime header so the
142
142
  // adapter functions (__tc_display, display_init, etc.) are available.
143
143
  const profile = getDisplayProfile();
144
- // 0. SPI-bus display drivers (ILI9341, ST7796, …) need the SPI library.
145
- // Host render targets (sdl, native-preview) have no SPI bus, so skip it.
146
- const spiDriver = profile?.driver !== "sdl" && profile?.driver !== "native-preview";
144
+ // 0. SPI-bus display drivers (ILI9341, ST7796, …) need the Arduino SPI
145
+ // library but ONLY on the Adafruit path. Native adapters (AVR, ESP32)
146
+ // emit their own bus setup and would break on <SPI.h>. Host render targets
147
+ // (sdl, native-preview) have no SPI bus at all.
148
+ const nativeAdapter = ctx.strategy?.providesDisplayAdapter?.() ?? false;
149
+ const spiDriver = profile?.driver !== "sdl"
150
+ && profile?.driver !== "native-preview"
151
+ && !nativeAdapter;
147
152
  if (spiDriver && !ctx.includes.includes("<SPI.h>")) {
148
153
  ctx.includes.push("<SPI.h>");
149
154
  }
@@ -151,7 +156,7 @@ export function emitUIRuntime(ctx) {
151
156
  // buildEmitterContext (setup.ts), NOT here — emitPreamble runs before this
152
157
  // function, so a push here would be too late to land in the output.
153
158
  if (profile) {
154
- const adapter = generateDisplayAdapter(profile);
159
+ const adapter = generateDisplayAdapter(profile, ctx.strategy);
155
160
  // Prepend includes to the very front — Arduino's auto-prototyper scans
156
161
  // the whole .ino and generates prototypes that reference GFXcanvas16
157
162
  // etc. before any #include, so the GFX header must come first.
@@ -164,6 +169,14 @@ export function emitUIRuntime(ctx) {
164
169
  ctx.sourceLines.push(is888
165
170
  ? "#define UI_COLOR_DEPTH 888\n#define UI_COLOR_T uint32_t\n#define UI_DIM_MASK 0x7F7F7Fu"
166
171
  : "#define UI_COLOR_DEPTH 565\n#define UI_COLOR_T uint16_t\n#define UI_DIM_MASK 0x7BEFu");
172
+ // Native display adapters (AVR, ESP32) instantiate CuttlefishGFX by value
173
+ // in their declaration block. The class MUST be defined before that —
174
+ // emit the cuttlefish-gfx slice here, between the color preamble and the
175
+ // adapter declaration. Arduino path (Adafruit) returns "" from the slice.
176
+ const nativeDisplayActive = ctx.strategy?.providesDisplayAdapter?.() ?? false;
177
+ if (nativeDisplayActive) {
178
+ ctx.sourceLines.push(ui.emitCuttlefishGfx(true));
179
+ }
167
180
  ctx.sourceLines.push(adapter.declaration);
168
181
  ctx.sourceLines.push(adapter.functions);
169
182
  }
@@ -173,7 +186,7 @@ export function emitUIRuntime(ctx) {
173
186
  if (profile.touch) {
174
187
  const t = profile.touch;
175
188
  if (t.library) {
176
- touchAdapter = generateTouchAdapter(t);
189
+ touchAdapter = generateTouchAdapter(t, ctx.strategy);
177
190
  // Prepend includes to the front (Arduino auto-prototyper safety).
178
191
  ctx.sourceLines.unshift(touchAdapter.includes[0]);
179
192
  for (let i = 1; i < touchAdapter.includes.length; i++) {
@@ -252,13 +265,15 @@ export function emitUIRuntime(ctx) {
252
265
  // header so its #ifndef guards adopt them. Source of truth:
253
266
  // resolveScrollConfig(profile.scroll). Defaults derive from the declared
254
267
  // touch hardware, so the demo (XPT2046) gets resistive+full with no config.
255
- const scroll = resolveScrollConfig(profile, { buildTarget: profile._buildTarget });
268
+ const scroll = resolveScrollConfig(profile, { buildTarget: profile._buildTarget, psram: profile._psram });
256
269
  ctx.sourceLines.push(`#define UI_SCROLL_MAX_OVERSCROLL ${scroll.maxOverscroll}`, `#define UI_SCROLL_STIFFNESS_X10 ${Math.round(scroll.stiffness * 10)}`, `#define UI_SCROLL_EDGE_SNAP_PX ${scroll.edgeSnapPx}`, `#define UI_SCROLL_DRAG_SCALE_X10 ${Math.round(scroll.dragScale * 10)}`, `#define UI_SCROLL_INPUT_TIER_CAPACITIVE ${scroll.inputTier === "capacitive" ? 1 : 0}`, `#define UI_SCROLL_INPUT_TIER_RESISTIVE ${scroll.inputTier === "resistive" ? 1 : 0}`, `#define UI_SCROLL_INPUT_TIER_NONE ${scroll.inputTier === "none" ? 1 : 0}`, `#define UI_SCROLL_RENDER_TIER_FULL ${scroll.renderTier === "full" ? 1 : 0}`, `#define UI_SCROLL_RENDER_TIER_CONSTRAINED ${scroll.renderTier === "constrained" ? 1 : 0}`, `#define UI_SCROLL_CANVAS_BUDGET_BYTES ${scroll.scrollCanvasBudgetBytes}`, ...(scroll.debug ? [`#define UI_SCROLL_DEBUG 1`] : []));
257
270
  // Forward-declare __ui_kb_set_onchange before the runtime header: the
258
271
  // header's keyboard-open function calls it, but the definition is emitted
259
272
  // later (section 8b). On native (single TU) this must precede the header.
260
273
  ctx.sourceLines.push(`void __ui_kb_set_onchange();`);
261
- ctx.sourceLines.push(emitRuntimeHeader());
274
+ ctx.sourceLines.push(emitRuntimeHeader({
275
+ nativeDisplayActive: ctx.strategy?.providesDisplayAdapter?.() ?? false,
276
+ }));
262
277
  // 1.5. Touch poll function (uses the adapter pattern). Body is generated by
263
278
  // generateTouchPollBody — extracted for unit-testability of the capacitive
264
279
  // vs resistive branch (FT6336U/SDL skip the minPressure gate).
@@ -10,6 +10,7 @@ import { accessorGetterName } from "./utils/cpp-helpers.js";
10
10
  import { INTEGRAL_CPP_TYPE_RE } from "./utils/cpp-helpers.js";
11
11
  import { renderPeripheralProperty } from "../mapping/peripheral-names.js";
12
12
  import { parseCppType, renderCppType, bareType, parsedIsPointer, parsedIsVector, needsCStrForStringLike } from "../api/shared/cpp-type-ir.js";
13
+ import { cppTypeForHalOp } from "./utils/hal-op-cpp-type.js";
13
14
  /**
14
15
  * Renders ExpressionIR nodes to C++ code strings.
15
16
  */
@@ -403,6 +404,8 @@ export class ExpressionRenderer {
403
404
  }
404
405
  case "unary":
405
406
  return expr.operator === "!" ? "bool" : this.inferExpressionCppType(expr.operand, knownVariableTypes);
407
+ case "hal-expr":
408
+ return cppTypeForHalOp(expr.operation.operation);
406
409
  default:
407
410
  return undefined;
408
411
  }
@@ -714,10 +717,16 @@ export class ExpressionRenderer {
714
717
  }
715
718
  case "hal-expr": {
716
719
  const rendered = this.render(expr, exprTransformer);
717
- // HAL expressions that resolve to string-like outputs use %s
718
720
  if (rendered.startsWith('"') || this.stringVarNames?.has(rendered)) {
719
721
  return { format: "%s", arg: rendered, estimatedLength: 32 };
720
722
  }
723
+ const cppType = cppTypeForHalOp(expr.operation.operation);
724
+ if (cppType && this.isStringLikeCppType(cppType)) {
725
+ return { format: "%s", arg: rendered, estimatedLength: 32 };
726
+ }
727
+ if (cppType === "bool") {
728
+ return { format: "%s", arg: `(${rendered} ? "true" : "false")`, estimatedLength: 5 };
729
+ }
721
730
  return { format: "%d", arg: rendered, estimatedLength: 12 };
722
731
  }
723
732
  case "method-call":
@@ -8,6 +8,7 @@
8
8
  // Types are imported from @typecad/cuttlefish/api/shared.
9
9
  // ---------------------------------------------------------------------------
10
10
  import { escapeCppStringLiteral, escapeSnprintfFormatFragment } from "../utils/strings.js";
11
+ import { cppTypeForHalOp } from "./utils/hal-op-cpp-type.js";
11
12
  import { parsedElementString, parsedIsStringLike } from "../api/shared/cpp-type-ir.js";
12
13
  // ---------------------------------------------------------------------------
13
14
  // Scope state management
@@ -369,6 +370,13 @@ export function inferSnprintfArg(expr, strategy, scopeState, renderExpression, p
369
370
  if (rendered.startsWith('"') || stringVarNames?.has(rendered)) {
370
371
  return { format: "%s", arg: rendered, estimatedLength: 32, preludeLines: [] };
371
372
  }
373
+ const cppType = cppTypeForHalOp(expr.operation.operation);
374
+ if (cppType && strategy.isStringLikeType(cppType)) {
375
+ return { format: "%s", arg: rendered, estimatedLength: 32, preludeLines: [] };
376
+ }
377
+ if (cppType === "bool") {
378
+ return { format: "%s", arg: `(${rendered} ? "true" : "false")`, estimatedLength: 5, preludeLines: [] };
379
+ }
372
380
  return { format: "%d", arg: rendered, estimatedLength: 12, preludeLines: [] };
373
381
  }
374
382
  default:
@@ -485,6 +485,19 @@ export class StatementRenderer {
485
485
  const rawStmt = statement.callee.slice('__RAW_STMT__'.length);
486
486
  return forHeader ? rawStmt : `${rawStmt.endsWith(';') ? rawStmt : rawStmt + ';'}`;
487
487
  }
488
+ // Awaited network markers (__WIFI_WAIT__/__HTTP_WAIT__) reaching the
489
+ // plain renderer means the await sits outside an async state machine
490
+ // (top-level await, or a position the state-machine splitter doesn't
491
+ // support). Fall back to the blocking form of the op they carry.
492
+ if ((statement.callee === "__WIFI_WAIT__" || statement.callee === "__HTTP_WAIT__" || statement.callee === "__BLE_WAIT__" || statement.callee === "__HAL_WAIT__")
493
+ && statement.args[0]?.kind === "hal-expr") {
494
+ const resolved = routeHALOp(statement.args[0].operation, this.strategy);
495
+ const code = resolved?.code ?? (resolved?.expression ? `${resolved.expression};` : undefined);
496
+ if (code) {
497
+ return forHeader ? code.replace(/;$/, "") : code;
498
+ }
499
+ return `/* unhandled awaited hal-op: ${statement.args[0].operation.operation} */`;
500
+ }
488
501
  // Handle emit() — compile-time C++ injection
489
502
  if (statement.callee === "__EMIT__") {
490
503
  const rawText = statement.args.map(arg => this.renderEmitArg(arg)).join("");