@typecad/cuttlefish 1.0.0-alpha.3 → 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 (123) hide show
  1. package/README.md +4 -4
  2. package/dist/api/shared/display-adapter.d.ts +2 -1
  3. package/dist/api/shared/display-adapter.js +8 -1
  4. package/dist/api/shared/display-adapters/sdl.js +9 -2
  5. package/dist/api/shared/display-profile.d.ts +20 -3
  6. package/dist/api/shared/display-profile.js +21 -6
  7. package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
  8. package/dist/api/shared/framework-manifest-registry.js +25 -0
  9. package/dist/api/shared/framework-manifest.d.ts +462 -0
  10. package/dist/api/shared/framework-manifest.js +149 -0
  11. package/dist/api/shared/glcdfont.d.ts +12 -0
  12. package/dist/api/shared/glcdfont.js +124 -0
  13. package/dist/api/shared/graphics-strategy.d.ts +28 -0
  14. package/dist/api/shared/hal-op-ir.d.ts +427 -1
  15. package/dist/api/shared/hal-op-ir.js +95 -1
  16. package/dist/api/shared/index.d.ts +11 -1
  17. package/dist/api/shared/index.js +14 -0
  18. package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
  19. package/dist/api/shared/native-display-op-resolver.js +64 -0
  20. package/dist/api/shared/platform-strategy.d.ts +9 -0
  21. package/dist/api/shared/promise-runtime.js +78 -0
  22. package/dist/api/shared/types.d.ts +8 -0
  23. package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
  24. package/dist/api/shared/validate-framework-manifest.js +417 -0
  25. package/dist/cli-utils.d.ts +1 -0
  26. package/dist/cli-utils.js +3 -1
  27. package/dist/cli.js +175 -4
  28. package/dist/config-loader.js +20 -1
  29. package/dist/config-schema.d.ts +36 -36
  30. package/dist/create/board-spec.d.ts +4 -4
  31. package/dist/create/index.d.ts +1 -1
  32. package/dist/create/index.js +1 -1
  33. package/dist/create/init-scaffold.d.ts +3 -0
  34. package/dist/create/init-scaffold.js +74 -2
  35. package/dist/create/init-templates.d.ts +4 -0
  36. package/dist/create/init-templates.js +217 -17
  37. package/dist/create/init-wizard.js +20 -1
  38. package/dist/emit/cpp-emitter.js +4 -3
  39. package/dist/emit/emitters/emitter-context.d.ts +5 -0
  40. package/dist/emit/emitters/function-emitter-impl.js +69 -59
  41. package/dist/emit/emitters/output-finalizer.d.ts +6 -0
  42. package/dist/emit/emitters/output-finalizer.js +21 -11
  43. package/dist/emit/emitters/setup.js +155 -0
  44. package/dist/emit/emitters/top-level-prep.js +2 -0
  45. package/dist/emit/emitters/ui-emitter.js +23 -8
  46. package/dist/emit/expression-renderer.js +10 -1
  47. package/dist/emit/snprintf-helpers.js +8 -0
  48. package/dist/emit/statement-renderer.js +13 -0
  49. package/dist/emit/utils/async-state-machine.js +185 -116
  50. package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
  51. package/dist/emit/utils/hal-op-cpp-type.js +40 -0
  52. package/dist/ir/adc-range-validation.js +26 -25
  53. package/dist/ir/build-ir.js +5 -1
  54. package/dist/ir/expression-to-ir.js +57 -6
  55. package/dist/ir/feature-registry.js +7 -25
  56. package/dist/ir/hal/hal-emitter.d.ts +5 -2
  57. package/dist/ir/hal/hal-emitter.js +40 -12
  58. package/dist/ir/hal/hal-parser.d.ts +6 -0
  59. package/dist/ir/hal/hal-parser.js +74 -0
  60. package/dist/ir/hal/hal-plugins.js +571 -0
  61. package/dist/ir/identifier-collector.js +18 -0
  62. package/dist/ir/interrupt-analysis.js +8 -3
  63. package/dist/ir/memory-budget-validation.js +1 -0
  64. package/dist/ir/network-validation.d.ts +4 -0
  65. package/dist/ir/network-validation.js +184 -0
  66. package/dist/ir/ownership-analysis.js +33 -1
  67. package/dist/ir/peripheral-ownership.js +5 -0
  68. package/dist/ir/peripheral-validation.d.ts +1 -1
  69. package/dist/ir/peripheral-validation.js +6 -3
  70. package/dist/ir/pin-alias-conflict.d.ts +1 -1
  71. package/dist/ir/pin-alias-conflict.js +2 -1
  72. package/dist/ir/pin-capability-validation.js +34 -32
  73. package/dist/ir/pin-mode-validation.js +5 -0
  74. package/dist/ir/pin-safety.d.ts +1 -1
  75. package/dist/ir/pin-safety.js +2 -1
  76. package/dist/ir/program-analysis.d.ts +39 -0
  77. package/dist/ir/program-analysis.js +192 -0
  78. package/dist/ir/pulldown-validation.d.ts +1 -1
  79. package/dist/ir/pulldown-validation.js +2 -1
  80. package/dist/ir/pwm-timer-sharing.d.ts +1 -1
  81. package/dist/ir/pwm-timer-sharing.js +2 -1
  82. package/dist/ir/resource-analysis.js +2 -0
  83. package/dist/ir/timer0-pwm-timing-conflict.d.ts +1 -1
  84. package/dist/ir/timer0-pwm-timing-conflict.js +2 -1
  85. package/dist/ir/timing-validation.d.ts +6 -1
  86. package/dist/ir/timing-validation.js +51 -12
  87. package/dist/ir/transformers/expressions.js +58 -0
  88. package/dist/ir/transformers/hal-emit-helpers.js +1 -1
  89. package/dist/ir/transformers/variables.js +86 -19
  90. package/dist/ir/try-catch-validation.js +2 -0
  91. package/dist/ir/type-resolution.js +2 -2
  92. package/dist/ir/unit-suspicion-validation.js +9 -7
  93. package/dist/ir/validation-orchestrator.js +9 -7
  94. package/dist/libdef/c-to-decl.d.ts +27 -0
  95. package/dist/libdef/c-to-decl.js +397 -0
  96. package/dist/libdef/component-decls.d.ts +2 -0
  97. package/dist/libdef/component-decls.js +6 -0
  98. package/dist/libdef/component-discovery.d.ts +43 -0
  99. package/dist/libdef/component-discovery.js +83 -0
  100. package/dist/libdef/cpp-to-decl.d.ts +9 -0
  101. package/dist/libdef/cpp-to-decl.js +72 -0
  102. package/dist/libdef/idf-discovery.d.ts +7 -0
  103. package/dist/libdef/idf-discovery.js +59 -0
  104. package/dist/libdef/registry.js +5 -2
  105. package/dist/licenses.d.ts +185 -0
  106. package/dist/licenses.js +963 -0
  107. package/dist/lint-cache.d.ts +59 -0
  108. package/dist/lint-cache.js +257 -0
  109. package/dist/orchestrator/graph-builder.js +6 -2
  110. package/dist/stores/display-profile-store.d.ts +1 -0
  111. package/dist/stores/display-profile-store.js +1 -0
  112. package/dist/testing.d.ts +4 -2
  113. package/dist/testing.js +4 -2
  114. package/dist/transpile.d.ts +3 -0
  115. package/dist/transpile.js +78 -32
  116. package/dist/types.d.ts +5 -1
  117. package/dist/ui-hook.d.ts +17 -1
  118. package/dist/utils/cli.js +71 -1
  119. package/dist/utils/fs.d.ts +13 -0
  120. package/dist/utils/fs.js +50 -0
  121. package/package.json +9 -4
  122. package/dist/ir/heap-array-validation.d.ts +0 -24
  123. package/dist/ir/heap-array-validation.js +0 -29
@@ -264,15 +264,52 @@ export function expressionToIR(expr, sourceText, diagnostics, pointerVars = new
264
264
  return `(sizeof(${safeText}) / sizeof(${safeText}[0]))`;
265
265
  }
266
266
  if (ts.isCallExpression(receiverNode)) {
267
- const returnType = ts.isPropertyAccessExpression(receiverNode.expression) && ts.isIdentifier(receiverNode.expression.expression)
268
- ? getCurrentIrTypeScope()?.locals.get(receiverNode.expression.expression.text)
269
- : undefined;
270
- if (returnType === "std::string")
271
- return `static_cast<long long>(${safeText}.length())`;
267
+ // Case B: a HAL buffer-returning method (I2CDevice.readBytes /
268
+ // SPIDevice.readRegister) used INLINE as a sub-expression — e.g.
269
+ // `dev.readBytes(0,6).length`. These methods lower to STATEMENTS (a fill
270
+ // loop), not a single C++ expression, so by the time we get here the
271
+ // receiver text is already a leaked `for (...) __buf[__i] = Wire.read()`
272
+ // statement spliced where an expression is required. The buffer also has
273
+ // no caller-side name to sizeof. Only the var-init form is supported
274
+ // (`const data = dev.readBytes(...)`); emit a clear diagnostic so the
275
+ // user gets an actionable error instead of inscrutable broken C++.
276
+ if (/\bfor\s*\(/.test(safeText) || /__buf|__spi_buf/.test(safeText)) {
277
+ const calleeName = ts.isPropertyAccessExpression(receiverNode.expression)
278
+ ? receiverNode.expression.name.text : "call";
279
+ diagnostics.push(makeDiagnostic(sourceText, receiverNode.getStart(), `\`${calleeName}(...)\` returns a buffer and cannot be queried inline. Capture it into a variable first (e.g. \`const data = ${calleeName}(...)\`), then use \`data.length\`.`, "error", "TC_BUFFER_INLINE_LENGTH"));
280
+ return `0 /* ${calleeName}() result must be captured into a variable to use .length */`;
281
+ }
282
+ // Resolve the call's RETURN type (not the receiver object's type, which
283
+ // the previous code incorrectly did). Method calls resolve via the class
284
+ // registry; free-function calls resolve by scanning the source AST for
285
+ // the declared return type.
286
+ const returnType = resolveExprCppType(receiverNode)
287
+ ?? resolveCallReturnTypeForNullGuard(receiverNode, sourceText);
288
+ if (returnType) {
289
+ const parsed = parseCppType(returnType);
290
+ // std::string → .length(); any STL container (vector/map/set) → .size().
291
+ if (parsedIsStdString(returnType)) {
292
+ return `static_cast<long long>(${safeText}.length())`;
293
+ }
294
+ if (parsedIsVector(returnType) || parsedIsMap(returnType) || parsedIsSet(returnType) || isContainer(parsed)) {
295
+ return `static_cast<long long>(${safeText}.size())`;
296
+ }
297
+ // A raw pointer / decayed-array return (e.g. uint8_t*) carries no size
298
+ // at the call site — sizeof would yield sizeof(pointer). This is
299
+ // genuinely un-sizeable inline; surface it rather than emit wrong code.
300
+ if (parsedIsPointer(returnType) || /\]\s*$/.test(returnType)) {
301
+ const calleeName = ts.isPropertyAccessExpression(receiverNode.expression)
302
+ ? receiverNode.expression.name.text
303
+ : (ts.isIdentifier(receiverNode.expression) ? receiverNode.expression.text : "call");
304
+ diagnostics.push(makeDiagnostic(sourceText, receiverNode.getStart(), `Cannot use \`.length\` on \`${calleeName}()\` which returns a pointer (${returnType}); the size is not available at the call site. Capture the buffer into a variable first.`, "error", "TC_LENGTH_ON_POINTER_RETURN"));
305
+ return `0 /* .length unavailable on ${returnType} return */`;
306
+ }
307
+ }
272
308
  // Cast .size() to long long to match the loop-counter type (TS number ->
273
309
  // long long). Without this, `i < vec.size()` compares long long vs
274
310
  // size_t (unsigned) and g++ -Wall warns -Wsign-compare on every
275
- // indexed loop over an array.
311
+ // indexed loop over an array. (Fallback for unresolvable return types —
312
+ // historically every call-result .length landed here.)
276
313
  return `static_cast<long long>(${safeText}.size())`;
277
314
  }
278
315
  // Resolve by concrete cppType first so std::string vars render member calls
@@ -916,6 +953,20 @@ export function expressionToIR(expr, sourceText, diagnostics, pointerVars = new
916
953
  };
917
954
  }
918
955
  if (ts.isCallExpression(expr)) {
956
+ // ---- rawCppExpr() — compile-time C++ injection in expression context ----
957
+ // Mirrors the statement-position rawCpp()/__EMIT__ path, but emits the raw
958
+ // text as an expression (e.g. for IDF macros like WIFI_INIT_CONFIG_DEFAULT()
959
+ // that produce struct values). The type parameter is a TS hint only — the
960
+ // transpiler emits the raw text verbatim.
961
+ if (ts.isIdentifier(expr.expression) && expr.expression.text === "rawCppExpr") {
962
+ const arg = expr.arguments[0];
963
+ if (arg && ts.isStringLiteral(arg)) {
964
+ return { kind: "raw", value: arg.text };
965
+ }
966
+ if (arg && ts.isNoSubstitutionTemplateLiteral(arg)) {
967
+ return { kind: "raw", value: arg.text };
968
+ }
969
+ }
919
970
  // ---- board() / boardResolve() — compile-time board constant lookup ----
920
971
  if (ts.isIdentifier(expr.expression) && (expr.expression.text === "board" || expr.expression.text === "boardResolve")) {
921
972
  const pathArg = expr.arguments[0];
@@ -198,11 +198,13 @@ function kindBasedLintRules() {
198
198
  // `no-restricted-syntax` selector for real-time editor warnings. Kept here so
199
199
  // that all editor-time selectors live in one place.
200
200
  //
201
- // NOTE on async/generator/yield severity: these are flagged as `"error"` by
202
- // ESLint (matching the prior hand-maintained config) but the transpiler only
203
- // emits a `TS2CPP_ASYNC_STUB` *warning* at build time and emits stub code.
204
- // The severity mismatch is intentional and documented here — it is not a bug
205
- // this refactor introduces or should silently "fix".
201
+ // NOTE on async/await: the transpiler fully supports these each `async`
202
+ // function is lowered to a cooperative state-machine task driven from
203
+ // `loop()` (see `emit/utils/async-state-machine.ts` and the
204
+ // `async-runtime-static.ts` microtask pump). `await expr` lowers to the task
205
+ // yielding between segments, so blocking waits become polls. They are
206
+ // therefore intentionally NOT linted here. Generators/yield and
207
+ // `for await...of` still have no lowering and remain flagged.
206
208
  const CONTEXT_LINT_RULES = [
207
209
  {
208
210
  selector: "ForOfStatement[await=true]",
@@ -244,26 +246,6 @@ const CONTEXT_LINT_RULES = [
244
246
  message: "[transpiler] .then() on a Promise is not supported (no promise runtime). Use synchronous return values or callbacks.",
245
247
  source: "context",
246
248
  },
247
- {
248
- selector: "FunctionDeclaration[async=true]",
249
- message: "[transpiler] async functions are recognized for parse-compatibility but cannot produce correct embedded behavior (no event loop). Use a synchronous function.",
250
- source: "context",
251
- },
252
- {
253
- selector: "FunctionExpression[async=true]",
254
- message: "[transpiler] async function expressions are recognized for parse-compatibility but cannot produce correct embedded behavior (no event loop). Use a synchronous function.",
255
- source: "context",
256
- },
257
- {
258
- selector: "ArrowFunctionExpression[async=true]",
259
- message: "[transpiler] async arrow functions are recognized for parse-compatibility but cannot produce correct embedded behavior (no event loop). Use a synchronous arrow function.",
260
- source: "context",
261
- },
262
- {
263
- selector: "AwaitExpression",
264
- message: "[transpiler] await is stripped to an inline expression — semantics are approximate and there is no event loop on bare metal. Avoid in firmware.",
265
- source: "context",
266
- },
267
249
  {
268
250
  selector: "FunctionDeclaration[generator=true]",
269
251
  message: "[transpiler] generator functions (function*) have no coroutine runtime on bare metal and are effectively unusable. Avoid.",
@@ -19,9 +19,12 @@ export declare function maybeEscapeResolvedText(text: string): string;
19
19
  export declare function stripCStrAfterStringLiteral(followingLiteralText: string, resolvedValue: string): string;
20
20
  /** Resolve an arbitrary expression to its text form, with this/param substitution. */
21
21
  export declare function resolveExpressionText(expr: ts.Expression, instance: HALInstance, paramNames: string[], callArgTexts: string[], paramDefaults?: Map<string, string>): string | null;
22
+ /** True when a HAL semantic call name wraps a GPIO/hardware ISR callback. */
23
+ export declare function semanticCallUsesIsrCallback(fnName: string): boolean;
22
24
  /** Scan an expression AST for callback() calls, extract callback IR from callArgs,
23
- * register them, and patch callArgTexts with placeholder names. */
24
- export declare function extractAndRegisterCallbacks(expr: ts.Expression, paramNames: string[], callArgs: ExpressionIR[], callArgTexts: string[]): void;
25
+ * register them, and patch callArgTexts with placeholder names.
26
+ * @param isInterruptHandler only true for GPIO interrupt attach paths. */
27
+ export declare function extractAndRegisterCallbacks(expr: ts.Expression, paramNames: string[], callArgs: ExpressionIR[], callArgTexts: string[], isInterruptHandler?: boolean): void;
25
28
  /** Process a HAL method body, resolving emit()/include()/semantic calls.
26
29
  * Returns { emitLines, halOps, returnValue, returnClassName } or null if unresolvable.
27
30
  *
@@ -5,6 +5,7 @@ import { renderExprAsText } from "../render-expr.js";
5
5
  import { escapeCppKeyword } from "../../utils/strings.js";
6
6
  import { halClassRegistry, halGlobalFunctions } from "./hal-parser.js";
7
7
  import { tryResolveSemanticCall, tryResolveBoardResolveArg, tryResolveCompoundSemanticReturn, resolveConcatPath } from "./hal-plugins.js";
8
+ import { cppTypeForHalOp } from "../../emit/utils/hal-op-cpp-type.js";
8
9
  /** Escape C++ keywords in resolved text, but only when the text looks like a
9
10
  * variable reference (not a literal like "false", "true", "42", or a string). */
10
11
  export function maybeEscapeResolvedText(text) {
@@ -275,9 +276,22 @@ export function resolveExpressionText(expr, instance, paramNames, callArgTexts,
275
276
  }
276
277
  return expr.getText ? expr.getText() : null;
277
278
  }
279
+ /** Semantic HAL helpers whose callbacks run in true ISR context (need IRAM_ATTR).
280
+ * WiFi event handlers, timers, setInterval, etc. use callback() too but run in
281
+ * a task / event loop — marking those as ISRs puts printf in IRAM and, on
282
+ * ESP-IDF, trips -Werror=attributes when IRAM_ATTR's __COUNTER__ disagrees
283
+ * between forward declaration and definition. */
284
+ const ISR_CALLBACK_SEMANTIC_FNS = new Set([
285
+ "interruptAttach",
286
+ ]);
287
+ /** True when a HAL semantic call name wraps a GPIO/hardware ISR callback. */
288
+ export function semanticCallUsesIsrCallback(fnName) {
289
+ return ISR_CALLBACK_SEMANTIC_FNS.has(fnName);
290
+ }
278
291
  /** Scan an expression AST for callback() calls, extract callback IR from callArgs,
279
- * register them, and patch callArgTexts with placeholder names. */
280
- export function extractAndRegisterCallbacks(expr, paramNames, callArgs, callArgTexts) {
292
+ * register them, and patch callArgTexts with placeholder names.
293
+ * @param isInterruptHandler only true for GPIO interrupt attach paths. */
294
+ export function extractAndRegisterCallbacks(expr, paramNames, callArgs, callArgTexts, isInterruptHandler = false) {
281
295
  function scan(node) {
282
296
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "callback") {
283
297
  const cbArg = node.arguments[0];
@@ -292,6 +306,8 @@ export function extractAndRegisterCallbacks(expr, paramNames, callArgs, callArgT
292
306
  kind: "callback",
293
307
  params: callbackIR.params.map((p) => p.name),
294
308
  statements: callbackIR.body,
309
+ ...(callbackIR.returnType && callbackIR.returnType !== "void" ? { returnType: callbackIR.returnType } : {}),
310
+ ...(callbackIR.params ? { typedParams: callbackIR.params.map((p) => ({ name: p.name, cppType: p.cppType })).filter((p) => p.cppType && p.cppType !== "void") } : {}),
295
311
  sourceSpan: callbackIR.body[0]?.sourceSpan ?? {
296
312
  filePath: "",
297
313
  startLine: 0,
@@ -301,10 +317,12 @@ export function extractAndRegisterCallbacks(expr, paramNames, callArgs, callArgT
301
317
  startOffset: 0,
302
318
  endOffset: 0,
303
319
  },
304
- isInterruptHandler: true,
320
+ ...(isInterruptHandler ? { isInterruptHandler: true } : {}),
305
321
  }
306
322
  : callbackIR;
307
- normalized.isInterruptHandler = true;
323
+ if (isInterruptHandler) {
324
+ normalized.isInterruptHandler = true;
325
+ }
308
326
  registeredCallbacks.push({ placeholderName: placeholder, callbackIR: normalized });
309
327
  callArgTexts[paramIdx] = placeholder;
310
328
  }
@@ -399,8 +417,9 @@ export function processHALMethodBody(instance, methodName, callArgs) {
399
417
  if (ts.isIdentifier(call.expression)) {
400
418
  // Extract callbacks from semantic call arguments and patch callArgTexts
401
419
  // with placeholder names before resolving (mirrors the emit() path).
420
+ const isIsr = semanticCallUsesIsrCallback(call.expression.text);
402
421
  for (const arg of call.arguments) {
403
- extractAndRegisterCallbacks(arg, paramNames, callArgs, callArgTexts);
422
+ extractAndRegisterCallbacks(arg, paramNames, callArgs, callArgTexts, isIsr);
404
423
  }
405
424
  const semanticOp = tryResolveSemanticCall(call.expression.text, call.arguments, instance, paramNames, callArgTexts, paramDefaults, callArgs);
406
425
  if (semanticOp) {
@@ -468,8 +487,9 @@ export function processHALMethodBody(instance, methodName, callArgs) {
468
487
  actualRetExpr = actualRetExpr.expression;
469
488
  }
470
489
  if (ts.isCallExpression(actualRetExpr) && ts.isIdentifier(actualRetExpr.expression)) {
490
+ const isIsr = semanticCallUsesIsrCallback(actualRetExpr.expression.text);
471
491
  for (const arg of actualRetExpr.arguments) {
472
- extractAndRegisterCallbacks(arg, paramNames, callArgs, callArgTexts);
492
+ extractAndRegisterCallbacks(arg, paramNames, callArgs, callArgTexts, isIsr);
473
493
  }
474
494
  const semanticOp = tryResolveSemanticCall(actualRetExpr.expression.text, actualRetExpr.arguments, instance, paramNames, callArgTexts, paramDefaults, callArgs);
475
495
  if (semanticOp) {
@@ -594,8 +614,9 @@ export function processStatementList(stmts, instance, paramNames, callArgTexts,
594
614
  }
595
615
  else {
596
616
  // R3: Try semantic HAL function call
617
+ const isIsr = semanticCallUsesIsrCallback(call.expression.text);
597
618
  for (const arg of call.arguments) {
598
- extractAndRegisterCallbacks(arg, paramNames, callArgs, callArgTexts);
619
+ extractAndRegisterCallbacks(arg, paramNames, callArgs, callArgTexts, isIsr);
599
620
  }
600
621
  const semanticOp = tryResolveSemanticCall(call.expression.text, call.arguments, instance, paramNames, callArgTexts, paramDefaults, callArgs);
601
622
  if (semanticOp) {
@@ -696,14 +717,21 @@ export function buildSnprintfFromConcat(expr) {
696
717
  else if (part.kind === "template_string" && part.expression.kind === "hal-expr") {
697
718
  // HAL expression inside template literal — resolve via strategy
698
719
  const resolved = resolveHALExprToText(part.expression);
699
- if (resolved !== null) {
700
- formatString += "%d";
701
- args.push(resolved);
702
- estimatedLength += 12;
720
+ const argText = resolved !== null ? resolved : text;
721
+ const cppType = cppTypeForHalOp(part.expression.operation.operation);
722
+ if (cppType === "const char*" || (cppType !== undefined && /char\s*\*$/.test(cppType))) {
723
+ formatString += "%s";
724
+ args.push(argText);
725
+ estimatedLength += 32;
726
+ }
727
+ else if (cppType === "bool") {
728
+ formatString += "%s";
729
+ args.push(`(${argText} ? "true" : "false")`);
730
+ estimatedLength += 5;
703
731
  }
704
732
  else {
705
733
  formatString += "%d";
706
- args.push(text);
734
+ args.push(argText);
707
735
  estimatedLength += 12;
708
736
  }
709
737
  }
@@ -50,6 +50,12 @@ export declare function extractMethods(cls: ts.ClassDeclaration): Map<string, HA
50
50
  export declare function loadHALModules(force?: boolean): void;
51
51
  /** Get constructor includes for a HAL class. */
52
52
  export declare function getCtorIncludes(className: string): string[];
53
+ /** Map an HttpClass factory method name to its HTTP verb, or null. */
54
+ export declare function httpFactoryVerb(methodName: string): string | null;
55
+ /** Render an Http factory URL argument as C++ expression text (string
56
+ * literals quoted, identifiers/member accesses verbatim), or null when the
57
+ * argument shape can't be resolved at compile time. */
58
+ export declare function httpUrlArgText(arg: ts.Expression): string | null;
53
59
  /** Resolve a call receiver to a tracked HAL instance. */
54
60
  export declare function resolveHALReceiver(receiver: ts.Expression): HALInstance | null;
55
61
  /** Build field values from constructor arguments using the field mapping.
@@ -210,6 +210,30 @@ export function loadHALModules(force = false) {
210
210
  export function getCtorIncludes(className) {
211
211
  return halCtorIncludes.get(className) ?? [];
212
212
  }
213
+ /** Map an HttpClass factory method name to its HTTP verb, or null. */
214
+ export function httpFactoryVerb(methodName) {
215
+ switch (methodName) {
216
+ case "get": return "GET";
217
+ case "post": return "POST";
218
+ case "put": return "PUT";
219
+ case "del": return "DELETE";
220
+ case "head": return "HEAD";
221
+ case "patch": return "PATCH";
222
+ default: return null;
223
+ }
224
+ }
225
+ /** Render an Http factory URL argument as C++ expression text (string
226
+ * literals quoted, identifiers/member accesses verbatim), or null when the
227
+ * argument shape can't be resolved at compile time. */
228
+ export function httpUrlArgText(arg) {
229
+ if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg))
230
+ return JSON.stringify(arg.text);
231
+ if (ts.isIdentifier(arg))
232
+ return arg.text;
233
+ if (ts.isPropertyAccessExpression(arg))
234
+ return arg.getText();
235
+ return null;
236
+ }
213
237
  /** Resolve a call receiver to a tracked HAL instance. */
214
238
  export function resolveHALReceiver(receiver) {
215
239
  const result = (() => {
@@ -336,6 +360,51 @@ export function resolveHALReceiver(receiver) {
336
360
  return { className: "ToneChain", fieldValues };
337
361
  }
338
362
  }
363
+ // Specialized handling for Http factory chaining
364
+ // (Http.get(url).header(...).send()): the factory records the HTTP
365
+ // verb and URL into the HttpRequest instance so send() can resolve
366
+ // this._method / this._url.
367
+ if (innerInstance.className === "HttpClass") {
368
+ const verb = httpFactoryVerb(methodName);
369
+ if (verb && receiver.arguments.length > 0) {
370
+ const urlText = httpUrlArgText(receiver.arguments[0]);
371
+ if (urlText) {
372
+ return {
373
+ className: "HttpRequest",
374
+ fieldValues: new Map([["_method", verb], ["_url", urlText]]),
375
+ };
376
+ }
377
+ }
378
+ }
379
+ // Specialized handling for BLE factory chaining
380
+ // (Ble.server(name).characteristic(uuid,type,perms).onRead(handler)):
381
+ // server() creates a BleServer with _name, _charCount from a global
382
+ // counter (persists across separate server() calls so multi-char
383
+ // servers get unique indices), _svcCount=1.
384
+ // characteristic() reads _charCount for the add_char call,
385
+ // then sets _lastChar=_charCount and increments _charCount so the next
386
+ // characteristic gets the next slot. onRead/onWrite read _lastChar.
387
+ if (innerInstance.className === "BleClass" && methodName === "server" && receiver.arguments.length > 0) {
388
+ const nameText = httpUrlArgText(receiver.arguments[0]);
389
+ if (nameText) {
390
+ return {
391
+ className: "BleServer",
392
+ fieldValues: new Map([["_name", nameText], ["_charCount", "0"], ["_lastChar", "0"], ["_svcCount", "1"]]),
393
+ };
394
+ }
395
+ }
396
+ if (innerInstance.className === "BleServer" && methodName === "characteristic") {
397
+ // characteristic(uuid, type, perms) returns this (BleServer).
398
+ // The C++ shim auto-assigns the char index at runtime via
399
+ // __tc_ble.current_char, so the resolver doesn't need to track indices.
400
+ return innerInstance;
401
+ }
402
+ if (innerInstance.className === "BleServer" && methodName === "service") {
403
+ // service() returns this (BleServer); advance the service counter.
404
+ const svc = Number(innerInstance.fieldValues.get("_svcCount") ?? "1") + 1;
405
+ innerInstance.fieldValues.set("_svcCount", String(svc));
406
+ return innerInstance;
407
+ }
339
408
  // General fallback: if the method is known to return another HAL class, carry over fields
340
409
  const classEntry = halClassRegistry.get(innerInstance.className);
341
410
  const methodEntry = classEntry?.methods.get(methodName);
@@ -345,6 +414,11 @@ export function resolveHALReceiver(receiver) {
345
414
  return { className: returnClassName, fieldValues: new Map(innerInstance.fieldValues) };
346
415
  }
347
416
  }
417
+ // Fluent `this`-returning methods (e.g. HttpRequest.header/timeout):
418
+ // the chain result is the same instance, so keep resolving through it.
419
+ if (methodEntry && methodEntry.methodNode.type && methodEntry.methodNode.type.kind === ts.SyntaxKind.ThisType) {
420
+ return innerInstance;
421
+ }
348
422
  }
349
423
  }
350
424
  // Inline new expression: new Pin(x).method()