@typecad/cuttlefish 0.1.0-alpha.1

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 (377) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -0
  3. package/dist/api/board-types.d.ts +1 -0
  4. package/dist/api/board-types.js +4 -0
  5. package/dist/api/capabilities.d.ts +12 -0
  6. package/dist/api/capabilities.js +1 -0
  7. package/dist/api/config.d.ts +120 -0
  8. package/dist/api/config.js +7 -0
  9. package/dist/api/index.d.ts +5 -0
  10. package/dist/api/index.js +2 -0
  11. package/dist/api/schema/builder.d.ts +14 -0
  12. package/dist/api/schema/builder.js +15 -0
  13. package/dist/api/schema/index.d.ts +2 -0
  14. package/dist/api/schema/index.js +1 -0
  15. package/dist/api/schema/types.d.ts +257 -0
  16. package/dist/api/schema/types.js +4 -0
  17. package/dist/api/shared/async-runtime-static.d.ts +8 -0
  18. package/dist/api/shared/async-runtime-static.js +201 -0
  19. package/dist/api/shared/async-types.d.ts +42 -0
  20. package/dist/api/shared/async-types.js +8 -0
  21. package/dist/api/shared/board-resolver.d.ts +12 -0
  22. package/dist/api/shared/board-resolver.js +7 -0
  23. package/dist/api/shared/cpp-type-ir.d.ts +229 -0
  24. package/dist/api/shared/cpp-type-ir.js +672 -0
  25. package/dist/api/shared/display-adapter.d.ts +12 -0
  26. package/dist/api/shared/display-adapter.js +145 -0
  27. package/dist/api/shared/display-adapters/eink-mono.d.ts +2 -0
  28. package/dist/api/shared/display-adapters/eink-mono.js +53 -0
  29. package/dist/api/shared/display-adapters/sdl.d.ts +2 -0
  30. package/dist/api/shared/display-adapters/sdl.js +452 -0
  31. package/dist/api/shared/display-adapters/ssd1309.d.ts +2 -0
  32. package/dist/api/shared/display-adapters/ssd1309.js +136 -0
  33. package/dist/api/shared/display-adapters/st7796.d.ts +2 -0
  34. package/dist/api/shared/display-adapters/st7796.js +132 -0
  35. package/dist/api/shared/display-capabilities.d.ts +45 -0
  36. package/dist/api/shared/display-capabilities.js +54 -0
  37. package/dist/api/shared/display-op-ir.d.ts +62 -0
  38. package/dist/api/shared/display-op-ir.js +19 -0
  39. package/dist/api/shared/display-profile.d.ts +265 -0
  40. package/dist/api/shared/display-profile.js +338 -0
  41. package/dist/api/shared/graphics-strategy.d.ts +29 -0
  42. package/dist/api/shared/graphics-strategy.js +11 -0
  43. package/dist/api/shared/hal-op-ir.d.ts +393 -0
  44. package/dist/api/shared/hal-op-ir.js +18 -0
  45. package/dist/api/shared/index.d.ts +26 -0
  46. package/dist/api/shared/index.js +21 -0
  47. package/dist/api/shared/ir-core.d.ts +363 -0
  48. package/dist/api/shared/ir-core.js +8 -0
  49. package/dist/api/shared/ir-declarations.d.ts +201 -0
  50. package/dist/api/shared/ir-declarations.js +26 -0
  51. package/dist/api/shared/ir.d.ts +85 -0
  52. package/dist/api/shared/ir.js +13 -0
  53. package/dist/api/shared/platform-strategy.d.ts +402 -0
  54. package/dist/api/shared/platform-strategy.js +14 -0
  55. package/dist/api/shared/polyfill-helper-registry.d.ts +11 -0
  56. package/dist/api/shared/polyfill-helper-registry.js +86 -0
  57. package/dist/api/shared/polyfill-types.d.ts +33 -0
  58. package/dist/api/shared/polyfill-types.js +76 -0
  59. package/dist/api/shared/promise-runtime.d.ts +10 -0
  60. package/dist/api/shared/promise-runtime.js +244 -0
  61. package/dist/api/shared/snprintf-types.d.ts +29 -0
  62. package/dist/api/shared/snprintf-types.js +8 -0
  63. package/dist/api/shared/string-method-registry.d.ts +53 -0
  64. package/dist/api/shared/string-method-registry.js +168 -0
  65. package/dist/api/shared/toolchain-types.d.ts +63 -0
  66. package/dist/api/shared/toolchain-types.js +71 -0
  67. package/dist/api/shared/types.d.ts +50 -0
  68. package/dist/api/shared/types.js +6 -0
  69. package/dist/ast/parse.d.ts +2 -0
  70. package/dist/ast/parse.js +4 -0
  71. package/dist/cache.d.ts +26 -0
  72. package/dist/cache.js +95 -0
  73. package/dist/cli-utils.d.ts +37 -0
  74. package/dist/cli-utils.js +250 -0
  75. package/dist/cli.d.ts +2 -0
  76. package/dist/cli.js +635 -0
  77. package/dist/config-loader.d.ts +78 -0
  78. package/dist/config-loader.js +508 -0
  79. package/dist/config-schema.d.ts +233 -0
  80. package/dist/config-schema.js +83 -0
  81. package/dist/create/board-checklist.d.ts +2 -0
  82. package/dist/create/board-checklist.js +52 -0
  83. package/dist/create/board-codegen.d.ts +9 -0
  84. package/dist/create/board-codegen.js +238 -0
  85. package/dist/create/board-generators.d.ts +12 -0
  86. package/dist/create/board-generators.js +652 -0
  87. package/dist/create/board-spec.d.ts +640 -0
  88. package/dist/create/board-spec.js +215 -0
  89. package/dist/create/eslint-rules-template.d.ts +2 -0
  90. package/dist/create/eslint-rules-template.js +672 -0
  91. package/dist/create/index.d.ts +11 -0
  92. package/dist/create/index.js +8 -0
  93. package/dist/create/init-scaffold.d.ts +27 -0
  94. package/dist/create/init-scaffold.js +150 -0
  95. package/dist/create/init-templates.d.ts +24 -0
  96. package/dist/create/init-templates.js +379 -0
  97. package/dist/create/init-wizard.d.ts +8 -0
  98. package/dist/create/init-wizard.js +168 -0
  99. package/dist/debug/breakpoint-loader.d.ts +24 -0
  100. package/dist/debug/breakpoint-loader.js +137 -0
  101. package/dist/debug/index.d.ts +3 -0
  102. package/dist/debug/index.js +7 -0
  103. package/dist/debug/preprocessor.d.ts +9 -0
  104. package/dist/debug/preprocessor.js +214 -0
  105. package/dist/debug/types.d.ts +32 -0
  106. package/dist/debug/types.js +6 -0
  107. package/dist/diagnostics/diagnostics-report.d.ts +51 -0
  108. package/dist/diagnostics/diagnostics-report.js +333 -0
  109. package/dist/diagnostics/json-schema.d.ts +138 -0
  110. package/dist/diagnostics/json-schema.js +27 -0
  111. package/dist/diagnostics/md-writer.d.ts +10 -0
  112. package/dist/diagnostics/md-writer.js +336 -0
  113. package/dist/diagnostics/mermaid-builder.d.ts +65 -0
  114. package/dist/diagnostics/mermaid-builder.js +681 -0
  115. package/dist/emit/cpp-emitter.d.ts +6 -0
  116. package/dist/emit/cpp-emitter.js +48 -0
  117. package/dist/emit/emitters/class-emitter.d.ts +2 -0
  118. package/dist/emit/emitters/class-emitter.js +523 -0
  119. package/dist/emit/emitters/emitter-context.d.ts +174 -0
  120. package/dist/emit/emitters/emitter-context.js +1 -0
  121. package/dist/emit/emitters/entrypoint-synthesizer.d.ts +2 -0
  122. package/dist/emit/emitters/entrypoint-synthesizer.js +175 -0
  123. package/dist/emit/emitters/function-emitter-impl.d.ts +19 -0
  124. package/dist/emit/emitters/function-emitter-impl.js +428 -0
  125. package/dist/emit/emitters/line-appender.d.ts +15 -0
  126. package/dist/emit/emitters/line-appender.js +344 -0
  127. package/dist/emit/emitters/namespace-emitter.d.ts +2 -0
  128. package/dist/emit/emitters/namespace-emitter.js +324 -0
  129. package/dist/emit/emitters/output-finalizer.d.ts +4 -0
  130. package/dist/emit/emitters/output-finalizer.js +267 -0
  131. package/dist/emit/emitters/setup.d.ts +3 -0
  132. package/dist/emit/emitters/setup.js +851 -0
  133. package/dist/emit/emitters/top-level-prep.d.ts +2 -0
  134. package/dist/emit/emitters/top-level-prep.js +706 -0
  135. package/dist/emit/emitters/type-decl-emitter.d.ts +2 -0
  136. package/dist/emit/emitters/type-decl-emitter.js +251 -0
  137. package/dist/emit/emitters/ui-emitter.d.ts +21 -0
  138. package/dist/emit/emitters/ui-emitter.js +564 -0
  139. package/dist/emit/enum-emitter.d.ts +1 -0
  140. package/dist/emit/enum-emitter.js +1 -0
  141. package/dist/emit/expression-renderer.d.ts +283 -0
  142. package/dist/emit/expression-renderer.js +1477 -0
  143. package/dist/emit/native-helpers-emitter.d.ts +11 -0
  144. package/dist/emit/native-helpers-emitter.js +34 -0
  145. package/dist/emit/route-hal-op.d.ts +5 -0
  146. package/dist/emit/route-hal-op.js +13 -0
  147. package/dist/emit/snprintf-helpers.d.ts +17 -0
  148. package/dist/emit/snprintf-helpers.js +462 -0
  149. package/dist/emit/statement-renderer.d.ts +189 -0
  150. package/dist/emit/statement-renderer.js +948 -0
  151. package/dist/emit/utils/async-state-machine.d.ts +42 -0
  152. package/dist/emit/utils/async-state-machine.js +343 -0
  153. package/dist/emit/utils/comment-helpers.d.ts +20 -0
  154. package/dist/emit/utils/comment-helpers.js +31 -0
  155. package/dist/emit/utils/cpp-helpers.d.ts +47 -0
  156. package/dist/emit/utils/cpp-helpers.js +85 -0
  157. package/dist/emit/utils/include-resolver.d.ts +58 -0
  158. package/dist/emit/utils/include-resolver.js +155 -0
  159. package/dist/emit/utils/index.d.ts +8 -0
  160. package/dist/emit/utils/index.js +12 -0
  161. package/dist/emit/utils/type-inference.d.ts +159 -0
  162. package/dist/emit/utils/type-inference.js +559 -0
  163. package/dist/eslint-check.d.ts +10 -0
  164. package/dist/eslint-check.js +102 -0
  165. package/dist/framework-package.d.ts +10 -0
  166. package/dist/framework-package.js +58 -0
  167. package/dist/framework-registry.d.ts +39 -0
  168. package/dist/framework-registry.js +30 -0
  169. package/dist/incremental-cache.d.ts +116 -0
  170. package/dist/incremental-cache.js +335 -0
  171. package/dist/ir/adc-range-validation.d.ts +11 -0
  172. package/dist/ir/adc-range-validation.js +250 -0
  173. package/dist/ir/ast-node-utils.d.ts +8 -0
  174. package/dist/ir/ast-node-utils.js +32 -0
  175. package/dist/ir/board-pin-utils.d.ts +13 -0
  176. package/dist/ir/board-pin-utils.js +65 -0
  177. package/dist/ir/board-resolver.d.ts +36 -0
  178. package/dist/ir/board-resolver.js +437 -0
  179. package/dist/ir/build-ir-state.d.ts +167 -0
  180. package/dist/ir/build-ir-state.js +306 -0
  181. package/dist/ir/build-ir.d.ts +2 -0
  182. package/dist/ir/build-ir.js +808 -0
  183. package/dist/ir/call-graph.d.ts +35 -0
  184. package/dist/ir/call-graph.js +194 -0
  185. package/dist/ir/compile-time-only.d.ts +2 -0
  186. package/dist/ir/compile-time-only.js +16 -0
  187. package/dist/ir/declaration-builders.d.ts +9 -0
  188. package/dist/ir/declaration-builders.js +608 -0
  189. package/dist/ir/entry-points.d.ts +30 -0
  190. package/dist/ir/entry-points.js +191 -0
  191. package/dist/ir/expression-to-ir.d.ts +5 -0
  192. package/dist/ir/expression-to-ir.js +2249 -0
  193. package/dist/ir/extract-property-chain.d.ts +10 -0
  194. package/dist/ir/extract-property-chain.js +30 -0
  195. package/dist/ir/feature-prescan.d.ts +3 -0
  196. package/dist/ir/feature-prescan.js +43 -0
  197. package/dist/ir/feature-registry.d.ts +30 -0
  198. package/dist/ir/feature-registry.js +816 -0
  199. package/dist/ir/filter.d.ts +7 -0
  200. package/dist/ir/filter.js +85 -0
  201. package/dist/ir/function-builder.d.ts +19 -0
  202. package/dist/ir/function-builder.js +670 -0
  203. package/dist/ir/hal/hal-emitter.d.ts +62 -0
  204. package/dist/ir/hal/hal-emitter.js +735 -0
  205. package/dist/ir/hal/hal-parser.d.ts +61 -0
  206. package/dist/ir/hal/hal-parser.js +473 -0
  207. package/dist/ir/hal/hal-plugins.d.ts +34 -0
  208. package/dist/ir/hal/hal-plugins.js +706 -0
  209. package/dist/ir/hal-resolver.d.ts +5 -0
  210. package/dist/ir/hal-resolver.js +5 -0
  211. package/dist/ir/heap-analysis.d.ts +8 -0
  212. package/dist/ir/heap-analysis.js +350 -0
  213. package/dist/ir/heap-array-validation.d.ts +24 -0
  214. package/dist/ir/heap-array-validation.js +29 -0
  215. package/dist/ir/identifier-collector.d.ts +18 -0
  216. package/dist/ir/identifier-collector.js +439 -0
  217. package/dist/ir/interrupt-analysis.d.ts +39 -0
  218. package/dist/ir/interrupt-analysis.js +467 -0
  219. package/dist/ir/memory-budget-validation.d.ts +10 -0
  220. package/dist/ir/memory-budget-validation.js +86 -0
  221. package/dist/ir/namespace-builder.d.ts +7 -0
  222. package/dist/ir/namespace-builder.js +152 -0
  223. package/dist/ir/ownership-analysis.d.ts +9 -0
  224. package/dist/ir/ownership-analysis.js +1207 -0
  225. package/dist/ir/peripheral-ownership.d.ts +3 -0
  226. package/dist/ir/peripheral-ownership.js +152 -0
  227. package/dist/ir/peripheral-symbols.d.ts +7 -0
  228. package/dist/ir/peripheral-symbols.js +57 -0
  229. package/dist/ir/peripheral-usage.d.ts +54 -0
  230. package/dist/ir/peripheral-usage.js +691 -0
  231. package/dist/ir/peripheral-validation.d.ts +8 -0
  232. package/dist/ir/peripheral-validation.js +77 -0
  233. package/dist/ir/pin-alias-conflict.d.ts +4 -0
  234. package/dist/ir/pin-alias-conflict.js +34 -0
  235. package/dist/ir/pin-capability-validation.d.ts +6 -0
  236. package/dist/ir/pin-capability-validation.js +413 -0
  237. package/dist/ir/pin-mode-validation.d.ts +10 -0
  238. package/dist/ir/pin-mode-validation.js +301 -0
  239. package/dist/ir/pin-safety.d.ts +11 -0
  240. package/dist/ir/pin-safety.js +58 -0
  241. package/dist/ir/program-analysis.d.ts +44 -0
  242. package/dist/ir/program-analysis.js +589 -0
  243. package/dist/ir/pulldown-validation.d.ts +7 -0
  244. package/dist/ir/pulldown-validation.js +49 -0
  245. package/dist/ir/pwm-timer-sharing.d.ts +4 -0
  246. package/dist/ir/pwm-timer-sharing.js +93 -0
  247. package/dist/ir/reachability.d.ts +68 -0
  248. package/dist/ir/reachability.js +224 -0
  249. package/dist/ir/register-decorators.d.ts +6 -0
  250. package/dist/ir/register-decorators.js +49 -0
  251. package/dist/ir/render-expr.d.ts +4 -0
  252. package/dist/ir/render-expr.js +127 -0
  253. package/dist/ir/resource-analysis.d.ts +19 -0
  254. package/dist/ir/resource-analysis.js +153 -0
  255. package/dist/ir/statement-to-ir.d.ts +23 -0
  256. package/dist/ir/statement-to-ir.js +349 -0
  257. package/dist/ir/symbol-table.d.ts +68 -0
  258. package/dist/ir/symbol-table.js +179 -0
  259. package/dist/ir/symbol-types.d.ts +56 -0
  260. package/dist/ir/symbol-types.js +107 -0
  261. package/dist/ir/timer0-pwm-timing-conflict.d.ts +4 -0
  262. package/dist/ir/timer0-pwm-timing-conflict.js +71 -0
  263. package/dist/ir/timing-validation.d.ts +7 -0
  264. package/dist/ir/timing-validation.js +95 -0
  265. package/dist/ir/transformers/array-methods.d.ts +8 -0
  266. package/dist/ir/transformers/array-methods.js +493 -0
  267. package/dist/ir/transformers/call-statement.d.ts +5 -0
  268. package/dist/ir/transformers/call-statement.js +758 -0
  269. package/dist/ir/transformers/callback-context-registry.d.ts +20 -0
  270. package/dist/ir/transformers/callback-context-registry.js +55 -0
  271. package/dist/ir/transformers/canvas-lowering.d.ts +42 -0
  272. package/dist/ir/transformers/canvas-lowering.js +249 -0
  273. package/dist/ir/transformers/control-flow.d.ts +8 -0
  274. package/dist/ir/transformers/control-flow.js +508 -0
  275. package/dist/ir/transformers/expressions.d.ts +7 -0
  276. package/dist/ir/transformers/expressions.js +406 -0
  277. package/dist/ir/transformers/hal-call-resolver.d.ts +29 -0
  278. package/dist/ir/transformers/hal-call-resolver.js +274 -0
  279. package/dist/ir/transformers/hal-emit-helpers.d.ts +14 -0
  280. package/dist/ir/transformers/hal-emit-helpers.js +63 -0
  281. package/dist/ir/transformers/namespace-methods.d.ts +20 -0
  282. package/dist/ir/transformers/namespace-methods.js +110 -0
  283. package/dist/ir/transformers/register-assignment.d.ts +5 -0
  284. package/dist/ir/transformers/register-assignment.js +56 -0
  285. package/dist/ir/transformers/ui-call-resolver.d.ts +149 -0
  286. package/dist/ir/transformers/ui-call-resolver.js +1136 -0
  287. package/dist/ir/transformers/ui-callback-lowering.d.ts +62 -0
  288. package/dist/ir/transformers/ui-callback-lowering.js +623 -0
  289. package/dist/ir/transformers/ui-mount.d.ts +20 -0
  290. package/dist/ir/transformers/ui-mount.js +35 -0
  291. package/dist/ir/transformers/ui-reactive.d.ts +68 -0
  292. package/dist/ir/transformers/ui-reactive.js +129 -0
  293. package/dist/ir/transformers/variables.d.ts +14 -0
  294. package/dist/ir/transformers/variables.js +1045 -0
  295. package/dist/ir/try-catch-validation.d.ts +26 -0
  296. package/dist/ir/try-catch-validation.js +128 -0
  297. package/dist/ir/type-resolution.d.ts +31 -0
  298. package/dist/ir/type-resolution.js +977 -0
  299. package/dist/ir/ui-element-auto-wire.d.ts +50 -0
  300. package/dist/ir/ui-element-auto-wire.js +218 -0
  301. package/dist/ir/unit-suspicion-validation.d.ts +12 -0
  302. package/dist/ir/unit-suspicion-validation.js +296 -0
  303. package/dist/ir/utils/walk-ir.d.ts +6 -0
  304. package/dist/ir/utils/walk-ir.js +162 -0
  305. package/dist/ir/validation-orchestrator.d.ts +4 -0
  306. package/dist/ir/validation-orchestrator.js +45 -0
  307. package/dist/libdef/base-class-resolver.d.ts +23 -0
  308. package/dist/libdef/base-class-resolver.js +50 -0
  309. package/dist/libdef/cpp-to-decl.d.ts +34 -0
  310. package/dist/libdef/cpp-to-decl.js +584 -0
  311. package/dist/libdef/header-parser.d.ts +53 -0
  312. package/dist/libdef/header-parser.js +269 -0
  313. package/dist/libdef/registry.d.ts +10 -0
  314. package/dist/libdef/registry.js +142 -0
  315. package/dist/mapping/peripheral-names.d.ts +23 -0
  316. package/dist/mapping/peripheral-names.js +63 -0
  317. package/dist/mapping/source-map.d.ts +46 -0
  318. package/dist/mapping/source-map.js +207 -0
  319. package/dist/orchestrator/dts-generator.d.ts +17 -0
  320. package/dist/orchestrator/dts-generator.js +83 -0
  321. package/dist/orchestrator/graph-builder.d.ts +18 -0
  322. package/dist/orchestrator/graph-builder.js +218 -0
  323. package/dist/orchestrator/semantic-facts-verifier.d.ts +38 -0
  324. package/dist/orchestrator/semantic-facts-verifier.js +121 -0
  325. package/dist/orchestrator/semantic-facts.d.ts +141 -0
  326. package/dist/orchestrator/semantic-facts.js +652 -0
  327. package/dist/orchestrator/type-checker.d.ts +39 -0
  328. package/dist/orchestrator/type-checker.js +760 -0
  329. package/dist/platform/async-runtime.d.ts +8 -0
  330. package/dist/platform/async-runtime.js +26 -0
  331. package/dist/platform/generic-strategy.d.ts +101 -0
  332. package/dist/platform/generic-strategy.js +289 -0
  333. package/dist/platform/index.d.ts +4 -0
  334. package/dist/platform/index.js +9 -0
  335. package/dist/platform/registry.d.ts +21 -0
  336. package/dist/platform/registry.js +42 -0
  337. package/dist/platform/toolchain.d.ts +4 -0
  338. package/dist/platform/toolchain.js +43 -0
  339. package/dist/preview/client.d.ts +1 -0
  340. package/dist/preview/client.js +137 -0
  341. package/dist/preview/server.d.ts +5 -0
  342. package/dist/preview/server.js +199 -0
  343. package/dist/profiler/index.d.ts +28 -0
  344. package/dist/profiler/index.js +28 -0
  345. package/dist/profiler/profiler.d.ts +95 -0
  346. package/dist/profiler/profiler.js +338 -0
  347. package/dist/profiler/types.d.ts +87 -0
  348. package/dist/profiler/types.js +12 -0
  349. package/dist/stores/display-profile-store.d.ts +16 -0
  350. package/dist/stores/display-profile-store.js +48 -0
  351. package/dist/stores/theme-store.d.ts +13 -0
  352. package/dist/stores/theme-store.js +30 -0
  353. package/dist/testing.d.ts +45 -0
  354. package/dist/testing.js +47 -0
  355. package/dist/transpile/resolution.d.ts +29 -0
  356. package/dist/transpile/resolution.js +282 -0
  357. package/dist/transpile.d.ts +3 -0
  358. package/dist/transpile.js +722 -0
  359. package/dist/types.d.ts +217 -0
  360. package/dist/types.js +1 -0
  361. package/dist/ui/ui-bridge.d.ts +4 -0
  362. package/dist/ui/ui-bridge.js +34 -0
  363. package/dist/ui-hook.d.ts +84 -0
  364. package/dist/ui-hook.js +45 -0
  365. package/dist/utils/cli.d.ts +3 -0
  366. package/dist/utils/cli.js +463 -0
  367. package/dist/utils/fs.d.ts +4 -0
  368. package/dist/utils/fs.js +23 -0
  369. package/dist/utils/logger.d.ts +2 -0
  370. package/dist/utils/logger.js +8 -0
  371. package/dist/utils/strings.d.ts +35 -0
  372. package/dist/utils/strings.js +142 -0
  373. package/dist/utils/ui.d.ts +66 -0
  374. package/dist/utils/ui.js +149 -0
  375. package/dist/watch.d.ts +35 -0
  376. package/dist/watch.js +111 -0
  377. package/package.json +127 -0
@@ -0,0 +1,1477 @@
1
+ /**
2
+ * Expression Renderer for C++ code emission.
3
+ * Encapsulates all expression rendering logic with explicit dependencies.
4
+ * Extracted from cpp-emitter.ts
5
+ */
6
+ import { routeHALOp } from "./route-hal-op.js";
7
+ import { extractPropertyChain } from "../ir/extract-property-chain.js";
8
+ import { escapeCppKeyword, escapeCppStringLiteral } from "../utils/strings.js";
9
+ import { accessorGetterName } from "./utils/cpp-helpers.js";
10
+ import { INTEGRAL_CPP_TYPE_RE } from "./utils/cpp-helpers.js";
11
+ import { renderPeripheralProperty } from "../mapping/peripheral-names.js";
12
+ import { parseCppType, renderCppType, bareType, parsedIsPointer, parsedIsVector, needsCStrForStringLike } from "../api/shared/cpp-type-ir.js";
13
+ /**
14
+ * Renders ExpressionIR nodes to C++ code strings.
15
+ */
16
+ export class ExpressionRenderer {
17
+ constructor(context) {
18
+ /** Accumulated snprintf prelude lines (buffer declarations, dtostrf calls, snprintf calls). */
19
+ this._preludeLines = [];
20
+ this.strategy = context.strategy;
21
+ this.boardConstants = context.boardConstants;
22
+ this.classNameMap = context.classNameMap;
23
+ this.enumNames = context.enumNames;
24
+ this.stringEnumNames = context.stringEnumNames ?? new Set();
25
+ this.largeEnumNames = context.largeEnumNames;
26
+ this.knownFunctionReturnTypes = context.knownFunctionReturnTypes;
27
+ this.knownVariableTypes = context.knownVariableTypes;
28
+ this.pointerVarTypes = context.pointerVarTypes;
29
+ this.globalPointerVarTypes = context.globalPointerVarTypes;
30
+ this.stringVarNames = context.stringVarNames;
31
+ this.cArrayVarNames = context.cArrayVarNames;
32
+ this.namespaceNames = context.namespaceNames ?? new Set();
33
+ this.varAccessorNames = context.varAccessorNames ?? new Map();
34
+ this.typeAccessorNames = context.typeAccessorNames ?? new Map();
35
+ this.interfaceFieldTypes = context.interfaceFieldTypes ?? new Map();
36
+ this.knownTopLevelObjectTypes = context.knownTopLevelObjectTypes;
37
+ this._snprintfCounter = context.snprintfCounter ?? { value: 0 };
38
+ this._diagnostics = context.diagnostics ?? [];
39
+ }
40
+ /**
41
+ * Gets the board constants (for access by statement renderer).
42
+ */
43
+ getBoardConstants() {
44
+ return this.boardConstants;
45
+ }
46
+ /**
47
+ * Clear accumulated prelude lines. Call before each top-level render.
48
+ */
49
+ clearPrelude() {
50
+ this._preludeLines = [];
51
+ }
52
+ /**
53
+ * Drain accumulated prelude lines and clear. The caller is responsible
54
+ * for emitting these lines before the statement that uses the expression.
55
+ */
56
+ drainPrelude() {
57
+ const lines = this._preludeLines;
58
+ this._preludeLines = [];
59
+ return lines;
60
+ }
61
+ /**
62
+ * Push lines directly into the prelude buffer. Used by the statement renderer
63
+ * to inject multi-statement expansions (e.g. Wire read setup) that must appear
64
+ * before the surrounding statement.
65
+ */
66
+ pushPrelude(lines) {
67
+ this._preludeLines.push(...lines);
68
+ }
69
+ /**
70
+ * Renders an expression IR node to a C++ string.
71
+ *
72
+ * @param expr The expression to render
73
+ * @param exprTransformer Optional transformer for raw expressions
74
+ * @param knownVariableTypes Optional override for variable type mapping
75
+ * @returns The C++ code string
76
+ */
77
+ render(expr, exprTransformer, knownVariableTypes) {
78
+ // Safety check
79
+ if (!expr || typeof expr !== 'object' || !expr.kind) {
80
+ return "/* invalid expression */";
81
+ }
82
+ let rendered;
83
+ switch (expr.kind) {
84
+ case "number": {
85
+ if (expr.cppType === "float" || !Number.isInteger(expr.value)) {
86
+ const str = `${expr.value}`;
87
+ rendered = str.includes('.') || str.includes('e') || str.includes('E')
88
+ ? `${str}f`
89
+ : `${str}.0f`;
90
+ break;
91
+ }
92
+ rendered = `${expr.value}`;
93
+ break;
94
+ }
95
+ case "string":
96
+ rendered = `"${expr.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
97
+ break;
98
+ case "boolean":
99
+ rendered = expr.value ? "true" : "false";
100
+ break;
101
+ case "identifier":
102
+ rendered = this.renderIdentifier(expr.value);
103
+ break;
104
+ case "raw":
105
+ rendered = this.renderRaw(expr.value, exprTransformer);
106
+ break;
107
+ case "await":
108
+ rendered = this.render(expr.value, exprTransformer);
109
+ break;
110
+ case "ternary":
111
+ rendered = this.renderTernary(expr, exprTransformer);
112
+ break;
113
+ case "string_concat":
114
+ rendered = this.renderStringConcat(expr, exprTransformer, knownVariableTypes);
115
+ break;
116
+ case "template_string":
117
+ rendered = this.renderTemplateString(expr, exprTransformer, knownVariableTypes);
118
+ break;
119
+ case "array":
120
+ rendered = this.renderArray(expr, exprTransformer);
121
+ break;
122
+ case "object":
123
+ rendered = this.renderObject(expr, exprTransformer);
124
+ break;
125
+ case "instanceof":
126
+ rendered = this.renderInstanceof(expr, exprTransformer);
127
+ break;
128
+ case "spread_array":
129
+ rendered = this.renderSpreadArray(expr, exprTransformer);
130
+ break;
131
+ case "paren":
132
+ rendered = `(${this.render(expr.inner, exprTransformer)})`;
133
+ break;
134
+ case "binary":
135
+ rendered = this.renderBinary(expr, exprTransformer, knownVariableTypes);
136
+ break;
137
+ case "unary":
138
+ rendered = this.renderUnary(expr, exprTransformer);
139
+ break;
140
+ case "property-access":
141
+ rendered = this.renderPropertyAccess(expr, exprTransformer, knownVariableTypes);
142
+ break;
143
+ case "callback":
144
+ rendered = this.renderCallback(expr);
145
+ break;
146
+ case "lambda":
147
+ rendered = this.renderLambda(expr, exprTransformer);
148
+ break;
149
+ case "method-call":
150
+ rendered = this.renderMethodCall(expr, exprTransformer);
151
+ break;
152
+ case "element-access":
153
+ rendered = this.renderElementAccess(expr, exprTransformer, knownVariableTypes);
154
+ break;
155
+ case "hal-expr": {
156
+ const resolved = routeHALOp(expr.operation, this.strategy);
157
+ if (resolved?.expression) {
158
+ rendered = resolved.expression;
159
+ }
160
+ else if (resolved?.code) {
161
+ // Strip trailing semicolon and a leading `return ` for expression
162
+ // context. HAL rawCpp definitions (e.g. Preferences.getString) bake
163
+ // in `return X;` for statement context; in expression context the
164
+ // `return` keyword is invalid (avr-g++: "expected primary-expression
165
+ // before 'return'") and would leak as
166
+ // `strcmp(return Preferences.getString(...), ...)`. Demo #33 Finding C.
167
+ rendered = resolved.code.replace(/;\s*$/, "").replace(/^\s*return\s+/, "");
168
+ }
169
+ else {
170
+ // Unregistered HAL op: surface as a warning so the user sees it, but
171
+ // keep HAL as an extensibility point. The bare comment is retained
172
+ // as a visual marker in the generated C++.
173
+ this._diagnostics.push({
174
+ severity: "warning",
175
+ code: "TS2CPP_UNHANDLED_HAL",
176
+ message: `HAL operation '${expr.operation.operation}' is not registered with the platform strategy; emitting a placeholder comment.`,
177
+ });
178
+ rendered = `/* unhandled hal-expr: ${expr.operation.operation} */`;
179
+ }
180
+ break;
181
+ }
182
+ default: {
183
+ // An ExpressionIR kind the renderer doesn't know how to render is a
184
+ // transpiler bug — the renderer should cover every kind the IR builders
185
+ // produce. This arm is unreachable in the normal transpileFile path
186
+ // (IR lowering marks unsupported expressions as errors and the build
187
+ // aborts before emit). Throw so any path that does reach here fails
188
+ // loudly instead of emitting "0 /* unsupported_expr */" into the C++.
189
+ const kind = expr.kind ?? "<unknown>";
190
+ throw new Error(`ExpressionRenderer: unsupported ExpressionIR kind '${kind}' reached emission. ` +
191
+ `This is a transpiler bug; the renderer is missing a case for this kind.`);
192
+ }
193
+ }
194
+ return normalizeRawExpression(rendered, this.strategy, this.classNameMap);
195
+ }
196
+ renderIdentifier(value) {
197
+ const nullVal = this.strategy.nullValue();
198
+ // expression-to-ir lowers the TS `null` literal to the identifier sentinel
199
+ // "nullptr" and `undefined` to "CUTTLEFISH_UNDEFINED". Both must route
200
+ // through nullValue() so the platform's chosen null representation is used
201
+ // — otherwise "nullptr" is mangled to "nullptr_" by escapeCppKeyword (since
202
+ // nullptr is a C++ keyword) and emitted as an undefined token.
203
+ if (nullVal && (value === "null" || value === "undefined" || value === "nullptr")) {
204
+ return nullVal;
205
+ }
206
+ // Platform value/function-like macros (e.g. Arduino INPUT, OUTPUT, HIGH,
207
+ // LOW) must pass through verbatim — escaping them to `OUTPUT_` would emit
208
+ // an undefined symbol since the Arduino header defines them as macros.
209
+ if (this.strategy.passthroughMacroNames().has(value)) {
210
+ return value;
211
+ }
212
+ return escapeCppKeyword(value, this.strategy.reservedNames());
213
+ }
214
+ renderRaw(value, exprTransformer) {
215
+ const effectiveClassNameMap = this.classNameMap;
216
+ let result = exprTransformer
217
+ ? normalizeRawExpression(exprTransformer(value), this.strategy, effectiveClassNameMap)
218
+ : normalizeRawExpression(value, this.strategy, effectiveClassNameMap);
219
+ const cArrayNames = this.cArrayVarNames ?? new Set();
220
+ result = result.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\.(?:length|size)(?:\(\))?/g, (match, varName) => {
221
+ if (cArrayNames.has(varName))
222
+ return `(sizeof(${varName}) / sizeof(${varName}[0]))`;
223
+ // Only C-string pointers require strlen(); std::string vars keep member-call syntax.
224
+ const varInfo = this.knownVariableTypes?.get(varName);
225
+ if (varInfo?.cppType === "const char*" || varInfo?.cppType === "char*")
226
+ return `strlen(${varName})`;
227
+ return match;
228
+ });
229
+ // Rewrite getter property access: s->reading → s->getReading()
230
+ for (const [varName, accessors] of this.varAccessorNames) {
231
+ for (const [propName, kind] of accessors) {
232
+ if (kind === "getter" || kind === "both") {
233
+ const getterName = accessorGetterName(propName);
234
+ const pattern = new RegExp(`\\b${varName}->${propName}\\b(?!\\()`, "g");
235
+ result = result.replace(pattern, `${varName}->${getterName}()`);
236
+ }
237
+ }
238
+ }
239
+ return result;
240
+ }
241
+ renderTernary(expr, exprTransformer) {
242
+ return `(${this.render(expr.condition, exprTransformer)} ? ${this.render(expr.whenTrue, exprTransformer)} : ${this.render(expr.whenFalse, exprTransformer)})`;
243
+ }
244
+ normalizeRecordType(cppType) {
245
+ // Strip const/pointer/reference qualifiers and unwrap smart-pointer
246
+ // wrappers, returning the bare underlying type name. Used to look up
247
+ // interface field types by struct name.
248
+ let ir = parseCppType(cppType);
249
+ if (ir.kind === "smartPointer")
250
+ ir = ir.inner;
251
+ return renderCppType(bareType(ir));
252
+ }
253
+ inferExpressionCppType(expr, knownVariableTypes) {
254
+ const effectiveKnownVariableTypes = knownVariableTypes ?? this.knownVariableTypes;
255
+ switch (expr.kind) {
256
+ case "string":
257
+ case "string_concat":
258
+ case "template_string":
259
+ return "std::string";
260
+ case "number":
261
+ return expr.cppType ?? (Number.isInteger(expr.value) ? "int" : "double");
262
+ case "boolean":
263
+ return "bool";
264
+ case "identifier": {
265
+ const known = effectiveKnownVariableTypes?.get(expr.value)?.cppType;
266
+ if (known)
267
+ return known;
268
+ // A bare identifier that resolves to a module-scope (file-global)
269
+ // pointer variable — e.g. an ISR-captured `const btn = new Button()`
270
+ // accessed inside a hoisted callback, where the local type map is
271
+ // empty. Its C++ type is a pointer (e.g. `Button*`), so member access
272
+ // must use `->`. Previously patched by the output-finalizer file-wide
273
+ // text sweep; now resolved structurally here.
274
+ const globalPtrType = this.globalPointerVarTypes?.get(expr.value);
275
+ if (globalPtrType)
276
+ return globalPtrType;
277
+ if (this.stringVarNames?.has(expr.value))
278
+ return "std::string";
279
+ return undefined;
280
+ }
281
+ case "await":
282
+ return this.inferExpressionCppType(expr.value, knownVariableTypes);
283
+ case "paren":
284
+ return this.inferExpressionCppType(expr.inner, knownVariableTypes);
285
+ case "ternary": {
286
+ // A ternary of two string LITERALS renders as `(c ? "a" : "b")` — a
287
+ // const char*, which has no .c_str() member. inferExpressionCppType
288
+ // returns "std::string" for a `string` IR node, and the equality check
289
+ // below would then short-circuit to "std::string", so the
290
+ // concat/snprintf path wraps the ternary in an invalid `.c_str()`
291
+ // (demo #34 Finding D). Guard the both-branches-are-string-literals
292
+ // case FIRST. Only literal string operands are narrowed; a branch that
293
+ // is a real std::string variable keeps the std::string widening.
294
+ const bothStringLiterals = expr.whenTrue.kind === "string" && expr.whenFalse.kind === "string";
295
+ if (bothStringLiterals)
296
+ return "const char*";
297
+ const whenTrue = this.inferExpressionCppType(expr.whenTrue, knownVariableTypes);
298
+ const whenFalse = this.inferExpressionCppType(expr.whenFalse, knownVariableTypes);
299
+ if (whenTrue && whenFalse && whenTrue === whenFalse)
300
+ return whenTrue;
301
+ if (this.strategy.isStringLikeType(whenTrue ?? "") || this.strategy.isStringLikeType(whenFalse ?? ""))
302
+ return "std::string";
303
+ if (whenTrue === "double" || whenTrue === "float" || whenFalse === "double" || whenFalse === "float")
304
+ return "double";
305
+ return whenTrue ?? whenFalse;
306
+ }
307
+ case "property-access": {
308
+ // String enum member access (e.g. Color.Red) resolves to a const char*,
309
+ // because string enums are lowered to namespaces of constexpr const char*.
310
+ if ((expr.isEnum || (expr.object.kind === "identifier" && this.stringEnumNames.has(expr.object.value)))
311
+ && expr.object.kind === "identifier"
312
+ && this.stringEnumNames.has(expr.object.value)) {
313
+ return "const char*";
314
+ }
315
+ // Numeric enum member access (e.g. Cell.Dead) resolves to the enum
316
+ // type itself, so a wrapping expression (ternary, paren, call result)
317
+ // whose branches are enum members infers to the enum and the
318
+ // enum↔integral storage boundary can fire. Without this, `Cell.Dead`
319
+ // inferred to `undefined` (Cell is an enum name, not a variable, so
320
+ // neither the known-variable map nor interface-field map hit), and a
321
+ // ternary `(c ? Cell.Dead : Cell.Alive)` stored into `uint8_t`
322
+ // storage missed the boundary cast. Demo #32 Finding A.
323
+ if (expr.object.kind === "identifier"
324
+ && this.enumNames.has(expr.object.value)
325
+ && !this.stringEnumNames.has(expr.object.value)) {
326
+ return expr.object.value;
327
+ }
328
+ if (expr.property === "length" || expr.property === "size")
329
+ return "int";
330
+ if (expr.object.kind === "raw" && expr.object.value === "this") {
331
+ return effectiveKnownVariableTypes?.get(expr.property)?.cppType;
332
+ }
333
+ const objectType = this.inferExpressionCppType(expr.object, knownVariableTypes);
334
+ if (!objectType)
335
+ return undefined;
336
+ return this.interfaceFieldTypes.get(this.normalizeRecordType(objectType))?.get(expr.property);
337
+ }
338
+ case "element-access": {
339
+ if (expr.elementType && expr.elementType !== "auto")
340
+ return expr.elementType;
341
+ const objectType = this.inferExpressionCppType(expr.object, knownVariableTypes);
342
+ if (!objectType)
343
+ return undefined;
344
+ // Element type of vector/staticArray/cArray, detected structurally.
345
+ // The bare StaticArray<T,N> spelling (without __tc_ prefix) also flows
346
+ // through here as a named template; elementOf doesn't cover it, so
347
+ // fall back to rendering the first arg.
348
+ const objIr = parseCppType(objectType);
349
+ if (objIr.kind === "named" && objIr.name === "StaticArray" && objIr.args?.[0]) {
350
+ return renderCppType(objIr.args[0]);
351
+ }
352
+ const elemIr = objIr.kind === "vector" || objIr.kind === "staticArray" || objIr.kind === "cArray"
353
+ ? objIr.element
354
+ : undefined;
355
+ return elemIr ? renderCppType(elemIr) : undefined;
356
+ }
357
+ case "array":
358
+ return `std::vector<${expr.elementType}>`;
359
+ case "method-call": {
360
+ const helper = expr.callee.match(/^__tc_(?:toUpperCase|toLowerCase|trim|replace|charAt|substring|slice|padStart|padEnd|repeat|jsonStringify)\b/);
361
+ if (helper)
362
+ return "std::string";
363
+ if (/^__tc_(?:startsWith|endsWith|includes)\b/.test(expr.callee))
364
+ return "bool";
365
+ if (/^__tc_(?:charCodeAt|indexOf|lastIndexOf)\b/.test(expr.callee))
366
+ return "int";
367
+ if (expr.cppType)
368
+ return expr.cppType;
369
+ // Look up the method's return type. The callee text may be a full
370
+ // receiver chain (`this->methodName`, `obj->methodName`), so strip
371
+ // any `->` / `.` prefix to get the bare method name, which is how
372
+ // class method return types are registered in setup.ts.
373
+ const bareName = expr.callee.replace(/^.*->|^.*\./, "");
374
+ return this.knownFunctionReturnTypes?.get(expr.callee)
375
+ ?? this.knownFunctionReturnTypes?.get(bareName);
376
+ }
377
+ case "raw": {
378
+ if (/^std::string\(/.test(expr.value) || /^__tc_(?:toUpperCase|toLowerCase|trim|replace|charAt|substring|slice|padStart|padEnd|repeat|jsonStringify)\b/.test(expr.value)) {
379
+ return "std::string";
380
+ }
381
+ if (/^__tc_(?:startsWith|endsWith|includes)\b/.test(expr.value))
382
+ return "bool";
383
+ if (/^__tc_(?:charCodeAt|indexOf|lastIndexOf)\b/.test(expr.value))
384
+ return "int";
385
+ if (/^std::(floor|ceil|round|abs|sqrt|sin|cos|tan|atan2|log|exp|pow|fmod)\b/.test(expr.value)) {
386
+ return "double";
387
+ }
388
+ const callMatch = expr.value.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
389
+ return callMatch ? this.knownFunctionReturnTypes?.get(callMatch[1]) : undefined;
390
+ }
391
+ case "binary": {
392
+ const booleanOperators = new Set(["==", "===", "!=", "!==", "<", "<=", ">", ">=", "&&", "||"]);
393
+ if (booleanOperators.has(expr.operator))
394
+ return "bool";
395
+ const leftType = this.inferExpressionCppType(expr.left, knownVariableTypes);
396
+ const rightType = this.inferExpressionCppType(expr.right, knownVariableTypes);
397
+ if (expr.operator === "+" && (this.isStringLikeCppType(leftType) || this.isStringLikeCppType(rightType))) {
398
+ return "std::string";
399
+ }
400
+ if (leftType === "double" || leftType === "float" || rightType === "double" || rightType === "float")
401
+ return "double";
402
+ return leftType ?? rightType;
403
+ }
404
+ case "unary":
405
+ return expr.operator === "!" ? "bool" : this.inferExpressionCppType(expr.operand, knownVariableTypes);
406
+ default:
407
+ return undefined;
408
+ }
409
+ }
410
+ /**
411
+ * True when a C++ type name denotes a string value: either the strategy's
412
+ * built-in string-like types (std::string, const char*, Arduino String, …)
413
+ * OR a TypeScript string-enum name, which is lowered to `const char*`.
414
+ */
415
+ isStringLikeCppType(cppType) {
416
+ return !!cppType && (this.strategy.isStringLikeType(cppType) || this.stringEnumNames.has(cppType));
417
+ }
418
+ shouldSkipStringWrap(expr, knownVariableTypes) {
419
+ const cppType = this.inferExpressionCppType(expr, knownVariableTypes);
420
+ return this.isStringLikeCppType(cppType);
421
+ }
422
+ /**
423
+ * Resolve an expression to its C++ type when possible (consulting
424
+ * `knownVariableTypes`, interface/class field type maps, enum/string-enum
425
+ * sets, etc.). Returns `undefined` when the type cannot be determined.
426
+ *
427
+ * Public so emit-layer callers (e.g. the switch emitter in line-appender)
428
+ * can make type-aware lowering decisions — see demo #16 gap #1: a `switch`
429
+ * on a struct field of enum type must not be wrapped in `std::string(...)`.
430
+ */
431
+ inferCppType(expr, knownVariableTypes) {
432
+ return this.inferExpressionCppType(expr, knownVariableTypes);
433
+ }
434
+ renderKnownStringValue(rendered, cppType) {
435
+ const normalized = this.strategy.normalizeCppType(cppType);
436
+ if (!this.strategy.useSnprintfForStrings() && (normalized === "const char*" || normalized === "char*")) {
437
+ return `std::string(${rendered})`;
438
+ }
439
+ return rendered;
440
+ }
441
+ renderStringConcat(expr, exprTransformer, knownVariableTypes) {
442
+ // When snprintf mode is active, build snprintf buffer instead of String() concatenation
443
+ if (this.strategy.useSnprintfForStrings()) {
444
+ const snprintfResult = this.buildSnprintfFromParts(expr.parts, exprTransformer, knownVariableTypes);
445
+ if (snprintfResult) {
446
+ return snprintfResult;
447
+ }
448
+ }
449
+ // Fallback: Build a String concatenation chain
450
+ const renderedParts = expr.parts.map(part => {
451
+ if (part.kind === "boolean") {
452
+ return `std::string(${part.value ? '"true"' : '"false"'})`;
453
+ }
454
+ const rendered = this.render(part, exprTransformer, knownVariableTypes);
455
+ if (part.kind === "string") {
456
+ return rendered;
457
+ }
458
+ if (part.kind === "template_string") {
459
+ return rendered;
460
+ }
461
+ if (this.shouldSkipStringWrap(part, knownVariableTypes)) {
462
+ return rendered;
463
+ }
464
+ if (part.kind === "identifier") {
465
+ const effectiveKnownVariableTypes = knownVariableTypes ?? this.knownVariableTypes;
466
+ const varInfo = effectiveKnownVariableTypes?.get(part.value);
467
+ if (varInfo?.cppType === "bool") {
468
+ return `(${part.value} ? "true" : "false")`;
469
+ }
470
+ }
471
+ // Doubles/floats need JS-compatible formatting; std::to_string appends trailing zeros (3.000000).
472
+ const partType = this.inferExpressionCppType(part, knownVariableTypes);
473
+ if (partType === "double" || partType === "float") {
474
+ const bufferName = `__cuttlefish_str_${++this._snprintfCounter.value}`;
475
+ this._preludeLines.push(`char ${bufferName}[32];`, `snprintf(${bufferName}, sizeof(${bufferName}), "%.15g", ${rendered});`);
476
+ return `std::string(${bufferName})`;
477
+ }
478
+ return this.strategy.wrapStringObject(rendered);
479
+ });
480
+ return renderedParts.join(" + ");
481
+ }
482
+ renderTemplateString(expr, exprTransformer, knownVariableTypes) {
483
+ // When snprintf mode is active, build snprintf buffer for single interpolation
484
+ if (this.strategy.useSnprintfForStrings()) {
485
+ const argInfo = this.inferFormatSpecifier(expr.expression, exprTransformer, knownVariableTypes);
486
+ if (argInfo) {
487
+ if (argInfo.preludeLines) {
488
+ this._preludeLines.push(...argInfo.preludeLines);
489
+ }
490
+ const bufferName = `__cuttlefish_str_${++this._snprintfCounter.value}`;
491
+ const estimatedLength = Math.max(argInfo.estimatedLength + 1, 16);
492
+ this._preludeLines.push(`char ${bufferName}[${estimatedLength}];`, `snprintf(${bufferName}, sizeof(${bufferName}), "${argInfo.format}", ${argInfo.arg});`);
493
+ return bufferName;
494
+ }
495
+ }
496
+ const inferredType = this.inferExpressionCppType(expr.expression, knownVariableTypes);
497
+ const rendered = this.render(expr.expression, exprTransformer, knownVariableTypes);
498
+ if (this.isStringLikeCppType(inferredType)) {
499
+ return this.renderKnownStringValue(rendered, inferredType ?? "");
500
+ }
501
+ if (expr.expression.kind === "boolean") {
502
+ return `std::string(${expr.expression.value ? '"true"' : '"false"'})`;
503
+ }
504
+ if (inferredType === "bool") {
505
+ return `std::string(${rendered} ? "true" : "false")`;
506
+ }
507
+ if (inferredType === "double" || inferredType === "float") {
508
+ const bufferName = `__cuttlefish_str_${++this._snprintfCounter.value}`;
509
+ this._preludeLines.push(`char ${bufferName}[32];`, `snprintf(${bufferName}, sizeof(${bufferName}), "%.15g", ${rendered});`);
510
+ return `std::string(${bufferName})`;
511
+ }
512
+ return this.strategy.wrapStringObject(rendered);
513
+ }
514
+ /**
515
+ * Build snprintf format string and args from string_concat parts.
516
+ * Returns the buffer name on success, or undefined if snprintf can't handle it.
517
+ */
518
+ buildSnprintfFromParts(parts, exprTransformer, knownVariableTypes) {
519
+ let formatString = "";
520
+ const args = [];
521
+ let estimatedLength = 1;
522
+ for (const part of parts) {
523
+ if (part.kind === "string") {
524
+ // Escape literal text for the snprintf format string. Backslash/quote
525
+ // escapes are for the C string literal; the % → %% escape is for
526
+ // snprintf itself, since a bare % in the format string starts a
527
+ // conversion specifier (e.g. `hp=${x}%` would otherwise emit
528
+ // "hp=%.15g%" — a dangling conversion). MUST run before the other
529
+ // escapes so the doubled %% isn't itself touched.
530
+ formatString += part.value.replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
531
+ estimatedLength += part.value.length;
532
+ continue;
533
+ }
534
+ const argInfo = this.inferFormatSpecifier(part, exprTransformer, knownVariableTypes);
535
+ if (argInfo) {
536
+ if (argInfo.preludeLines) {
537
+ this._preludeLines.push(...argInfo.preludeLines);
538
+ }
539
+ formatString += argInfo.format;
540
+ args.push(argInfo.arg);
541
+ estimatedLength += argInfo.estimatedLength;
542
+ continue;
543
+ }
544
+ // Can't handle other part types with snprintf
545
+ return undefined;
546
+ }
547
+ const bufferName = `__cuttlefish_str_${++this._snprintfCounter.value}`;
548
+ estimatedLength = Math.max(estimatedLength, 16);
549
+ this._preludeLines.push(`char ${bufferName}[${estimatedLength}];`, `snprintf(${bufferName}, sizeof(${bufferName}), "${formatString}"${args.length > 0 ? `, ${args.join(", ")}` : ""});`);
550
+ return bufferName;
551
+ }
552
+ /**
553
+ * Infer printf format specifier for an expression.
554
+ * Returns format string, rendered arg, estimated length, and optional prelude lines, or undefined if unknown.
555
+ */
556
+ inferFormatSpecifier(expr, exprTransformer, knownVariableTypes) {
557
+ const effectiveKnownVariableTypes = knownVariableTypes ?? this.knownVariableTypes;
558
+ if (expr.kind === "template_string") {
559
+ return this.inferFormatSpecifier(expr.expression, exprTransformer, knownVariableTypes);
560
+ }
561
+ // Namespace-const access (`Ns.MEMBER`) lowers to a `property-access` IR
562
+ // node whose `property` is the member name. Resolve the member's type from
563
+ // knownVariableTypes (namespace consts are registered there by bare name
564
+ // in setup.ts) so a namespace `const string` operand formats as %s with
565
+ // .c_str(), not the %d default (namespace stress test Finding 3b).
566
+ if (expr.kind === "property-access") {
567
+ const memberType = effectiveKnownVariableTypes?.get(expr.property);
568
+ if (memberType) {
569
+ const rendered = this.render(expr, exprTransformer, knownVariableTypes);
570
+ if (this.isStringLikeCppType(memberType.cppType)) {
571
+ const normalized = this.strategy.normalizeCppType(memberType.cppType);
572
+ const needsCStr = needsCStrForStringLike(normalized);
573
+ return { format: "%s", arg: needsCStr ? `${rendered}.c_str()` : rendered, estimatedLength: 128 };
574
+ }
575
+ if (memberType.cppType === "bool") {
576
+ return { format: "%s", arg: `(${rendered} ? "true" : "false")`, estimatedLength: 5 };
577
+ }
578
+ }
579
+ }
580
+ if (expr.kind !== "number" && expr.kind !== "boolean" && expr.kind !== "string") {
581
+ const inferredType = this.inferExpressionCppType(expr, knownVariableTypes);
582
+ if (inferredType) {
583
+ const rendered = this.render(expr, exprTransformer, knownVariableTypes);
584
+ const normalized = this.strategy.normalizeCppType(inferredType);
585
+ if (this.isStringLikeCppType(inferredType)) {
586
+ // .c_str() is needed for std::string-like types (which own a buffer)
587
+ // but NOT for const char* / char* (already C-strings). Detect via the
588
+ // structured IR: string/strPtr/string-enum need it; char pointers don't.
589
+ const needsCStr = needsCStrForStringLike(normalized);
590
+ // std::string operands can be arbitrarily long (a method like
591
+ // statusLine() may return a 100+ char string), so budget a generous
592
+ // estimate. The previous value (32) truncated output whenever a
593
+ // string-returning method was interpolated into a concat.
594
+ return { format: "%s", arg: needsCStr ? `${rendered}.c_str()` : rendered, estimatedLength: 128 };
595
+ }
596
+ if (normalized === "bool") {
597
+ return { format: "%s", arg: `(${rendered} ? "true" : "false")`, estimatedLength: 5 };
598
+ }
599
+ if (normalized === "float" || normalized === "double") {
600
+ const knownPrecision = expr.kind === "identifier"
601
+ ? effectiveKnownVariableTypes?.get(expr.value)?.floatPrecision
602
+ : undefined;
603
+ const floatArg = this.strategy.floatToSnprintfArg?.(rendered, knownPrecision, ++this._snprintfCounter.value);
604
+ if (floatArg !== undefined)
605
+ return floatArg;
606
+ // Use %.15g (not %g) so large integer-valued doubles don't collapse
607
+ // to scientific notation — matches JS Number.toString() more closely.
608
+ return { format: knownPrecision !== undefined ? `%.${knownPrecision}f` : "%.15g", arg: rendered, estimatedLength: 16 };
609
+ }
610
+ // Enum-typed value (struct field, variable, etc. whose type resolves
611
+ // to a known numeric enum name). C++ enum class values need
612
+ // static_cast<int>(...) for %d — without it, -Wformat= warns that
613
+ // the argument type (enum) doesn't match %d (int).
614
+ if (this.enumNames.has(normalized) && !this.stringEnumNames.has(normalized)) {
615
+ return { format: "%d", arg: `static_cast<int>(${rendered})`, estimatedLength: 12 };
616
+ }
617
+ if (/^(?:unsigned\s+)?(?:char|short|int|long|long long)$/.test(normalized) || /^(?:u?int(?:8|16|32|64)_t|size_t)$/.test(normalized)) {
618
+ if (normalized.includes("long long") || /64_t$/.test(normalized)) {
619
+ return { format: "%lld", arg: rendered, estimatedLength: 20 };
620
+ }
621
+ // `long` is its own C++ type and matches %ld regardless of width.
622
+ // int32_t/uint32_t are typedefs for int/unsigned int on every target
623
+ // cuttlefish supports (AVR/ARM), so they must use %d/%u — using %ld
624
+ // triggers -Wformat= ("expects long int, has int").
625
+ if (normalized.includes("long")) {
626
+ return { format: normalized.startsWith("unsigned") ? "%lu" : "%ld", arg: rendered, estimatedLength: 12 };
627
+ }
628
+ // 32_t/16_t/8_t and bare int/short/char. These are `int`-sized or
629
+ // narrower; unsigned variants are `unsigned int`-sized → %u. size_t
630
+ // is platform-dependent but unsigned int on the targets here → %u.
631
+ if (/^size_t$/.test(normalized) || normalized.startsWith("uint") || normalized.startsWith("unsigned")) {
632
+ return { format: "%u", arg: rendered, estimatedLength: 12 };
633
+ }
634
+ return { format: "%d", arg: rendered, estimatedLength: 12 };
635
+ }
636
+ }
637
+ }
638
+ switch (expr.kind) {
639
+ case "number": {
640
+ if (expr.cppType === "float" || !Number.isInteger(expr.value)) {
641
+ const str = `${expr.value}`;
642
+ const rendered = str.includes('.') || str.includes('e') || str.includes('E')
643
+ ? `${str}f`
644
+ : `${str}.0f`;
645
+ return { format: "%.15g", arg: rendered, estimatedLength: 16 };
646
+ }
647
+ return { format: "%d", arg: `${expr.value}`, estimatedLength: 12 };
648
+ }
649
+ case "boolean":
650
+ return { format: "%s", arg: expr.value ? '"true"' : '"false"', estimatedLength: 5 };
651
+ case "string":
652
+ // Route through the shared `escapeCppStringLiteral` (handles `\`, `"`,
653
+ // `\n`, `\r`, `\t`) so a string-literal template/concat part carrying a
654
+ // control char renders as the escape sequence, not a raw char inside the
655
+ // C++ string literal. Demo #29 Finding A sibling — this path was reached
656
+ // by a standalone `${'x\ny'}` template part: the partial escape here
657
+ // (only `\` and `"`) emitted a raw newline → an unterminated C++ literal.
658
+ return { format: "%s", arg: `"${escapeCppStringLiteral(expr.value)}"`, estimatedLength: Math.max(expr.value.length, 1) };
659
+ case "identifier": {
660
+ const knownVar = effectiveKnownVariableTypes?.get(expr.value);
661
+ const cppType = knownVar?.cppType ?? this.knownFunctionReturnTypes?.get(expr.value);
662
+ if (this.isStringLikeCppType(cppType)) {
663
+ const normalized = this.strategy.normalizeCppType(cppType ?? "");
664
+ const arg = normalized === "__tc_str_ptr" ? `${expr.value}.c_str()` : expr.value;
665
+ return { format: "%s", arg, estimatedLength: 128 };
666
+ }
667
+ if (cppType === "bool") {
668
+ return { format: "%s", arg: `(${expr.value} ? "true" : "false")`, estimatedLength: 5 };
669
+ }
670
+ if (cppType === "float" || cppType === "double") {
671
+ const floatArg = this.strategy.floatToSnprintfArg?.(expr.value, knownVar?.floatPrecision, ++this._snprintfCounter.value);
672
+ if (floatArg !== undefined)
673
+ return floatArg;
674
+ return { format: knownVar?.floatPrecision !== undefined ? `%.${knownVar.floatPrecision}f` : "%.15g", arg: expr.value, estimatedLength: 16 };
675
+ }
676
+ if (cppType === "int" || cppType === "short" || cppType === "int16_t" || cppType === "uint16_t" || cppType === "int32_t") {
677
+ return { format: "%d", arg: expr.value, estimatedLength: 12 };
678
+ }
679
+ if (cppType === "uint32_t") {
680
+ return { format: "%u", arg: expr.value, estimatedLength: 12 };
681
+ }
682
+ // auto-deduced local: infer the real type from the retained initializer
683
+ // before falling back. Treating `auto` as `%ld` is wrong for locals
684
+ // deduced from a string/enum/float expression (e.g.
685
+ // `const name = loot.name` → std::string). Re-dispatch through the
686
+ // general inference path so the proper specifier is chosen.
687
+ if (cppType === "auto" && knownVar?.initializer) {
688
+ const inferredFromInit = this.inferFormatSpecifier(knownVar.initializer, exprTransformer, knownVariableTypes);
689
+ if (inferredFromInit) {
690
+ // Replace the placeholder arg with the local variable name, since
691
+ // the initializer's rendered form would re-evaluate side effects.
692
+ return { ...inferredFromInit, arg: expr.value };
693
+ }
694
+ }
695
+ if (cppType === "long") {
696
+ return { format: "%ld", arg: expr.value, estimatedLength: 12 };
697
+ }
698
+ if (cppType === "unsigned long") {
699
+ return { format: "%lu", arg: expr.value, estimatedLength: 12 };
700
+ }
701
+ if (cppType === "long long" || cppType === "unsigned long long" || cppType === "int64_t" || cppType === "uint64_t") {
702
+ return { format: "%lld", arg: expr.value, estimatedLength: 20 };
703
+ }
704
+ if (this.stringVarNames?.has(expr.value)) {
705
+ return { format: "%s", arg: expr.value, estimatedLength: 32 };
706
+ }
707
+ // Numeric enum-typed variable: static_cast<int> for snprintf (%d).
708
+ // String enums are const char* (handled by isStringLikeType above).
709
+ if (cppType && this.enumNames.has(cppType) && !this.stringEnumNames.has(cppType)) {
710
+ return { format: "%d", arg: `static_cast<int>(${expr.value})`, estimatedLength: 12 };
711
+ }
712
+ // Default to %d for integers and unknowns
713
+ return { format: "%d", arg: expr.value, estimatedLength: 12 };
714
+ }
715
+ case "hal-expr": {
716
+ const rendered = this.render(expr, exprTransformer);
717
+ // HAL expressions that resolve to string-like outputs use %s
718
+ if (rendered.startsWith('"') || this.stringVarNames?.has(rendered)) {
719
+ return { format: "%s", arg: rendered, estimatedLength: 32 };
720
+ }
721
+ return { format: "%d", arg: rendered, estimatedLength: 12 };
722
+ }
723
+ case "method-call":
724
+ case "property-access":
725
+ case "binary":
726
+ case "unary":
727
+ case "ternary":
728
+ case "raw": {
729
+ const rendered = this.render(expr, exprTransformer);
730
+ // String-returning helpers (__tc_toUpperCase, etc.) use %s.
731
+ // On hosted targets these return std::string, so snprintf needs
732
+ // .c_str(). On targets WITHOUT std::string (e.g. Arduino AVR) the
733
+ // helpers already return const char*, so .c_str() would be a redundant
734
+ // member access on a non-class type (avr-g++: "request for member
735
+ // 'c_str' in '__tc_trim(...)', which is of non-class type 'const
736
+ // char*'"). Demo #35 Finding A.
737
+ if (/^__tc_(toUpperCase|toLowerCase|trim|replace|charAt|substring|slice|padStart|padEnd|repeat|jsonStringify)\b/.test(rendered)) {
738
+ const needsCStr = this.strategy.needsStdString();
739
+ return { format: "%s", arg: needsCStr ? `${rendered}.c_str()` : rendered, estimatedLength: 32 };
740
+ }
741
+ // Check if it's a property access on a string (e.g. s.length)
742
+ if (expr.kind === "property-access" && (expr.property === "length" || expr.property === "size")) {
743
+ // Demo #30 Finding B — `.length`/`.size` now ALWAYS render as
744
+ // `static_cast<long long>(...)` (the array/vector `.size()` path was
745
+ // already cast; the std::string `.length()` path was cast in the same
746
+ // demo so the two are uniform). The rendered arg is therefore a
747
+ // signed `long long`, so the format specifier MUST be `%lld`. The
748
+ // previous hardcoded `%d` mismatched the unsigned `size_type` returned
749
+ // by `std::string::length()`, triggering g++ -Wformat=
750
+ // ("expects int, has size_type").
751
+ return { format: "%lld", arg: rendered, estimatedLength: 20 };
752
+ }
753
+ // Demo #30 Finding B (raw-node sibling) — `.length`/`.size` on an
754
+ // array/vector/string receiver lowers to a `raw` IR node whose text is
755
+ // `static_cast<long long>(x.size())` / `static_cast<long long>(s.length())`
756
+ // (the lowering in resolveLengthProperty casts to long long). That raw
757
+ // text is a signed `long long`, so the snprintf specifier must be `%lld`
758
+ // — the previous fall-through to the `%d` default mismatched the cast
759
+ // type and tripped g++ -Wformat=. This catches the lowered form; the
760
+ // `property-access` branch above catches the (rare) un-lowered form.
761
+ if (expr.kind === "raw" && /^static_cast<long long>\(.*\.(?:size|length)\(\)\)$/.test(rendered)) {
762
+ return { format: "%lld", arg: rendered, estimatedLength: 20 };
763
+ }
764
+ // String enum member access → const char* → %s with c_str() for snprintf.
765
+ if (expr.kind === "property-access" && expr.object.kind === "identifier"
766
+ && this.stringEnumNames.has(expr.object.value)) {
767
+ return { format: "%s", arg: `${rendered}.c_str()`, estimatedLength: 32 };
768
+ }
769
+ // Numeric enum member access → static_cast<int> for %d.
770
+ if (expr.kind === "property-access" && expr.object.kind === "identifier"
771
+ && this.enumNames.has(expr.object.value) && !this.stringEnumNames.has(expr.object.value)) {
772
+ return { format: "%d", arg: `static_cast<int>(${rendered})`, estimatedLength: 12 };
773
+ }
774
+ // Fallback for expressions that render as string-like pointers
775
+ if (this.stringVarNames?.has(rendered)) {
776
+ return { format: "%s", arg: rendered, estimatedLength: 32 };
777
+ }
778
+ // Default to %d for other complex expressions (mostly numeric)
779
+ return { format: "%d", arg: rendered, estimatedLength: 12 };
780
+ }
781
+ default:
782
+ return undefined;
783
+ }
784
+ }
785
+ renderArray(expr, exprTransformer) {
786
+ const elements = expr.elements.map((e) => this.render(e, exprTransformer)).join(", ");
787
+ return `{ ${elements} }`;
788
+ }
789
+ renderObject(expr, exprTransformer) {
790
+ const fields = expr.fields.map((f) => `${this.render(f.value, exprTransformer)}`).join(", ");
791
+ return `{ ${fields} }`;
792
+ }
793
+ renderSpreadArray(expr, exprTransformer) {
794
+ const spreadText = this.render(expr.spreadExpr, exprTransformer);
795
+ const elementType = expr.elementType === "auto" ? "auto" : expr.elementType;
796
+ const extras = expr.additionalElements.map(e => this.render(e, exprTransformer)).join(", ");
797
+ if (extras.length === 0)
798
+ return spreadText;
799
+ const combined = [spreadText, extras].join(", ");
800
+ return `std::vector<${elementType}>({ ${combined} })`;
801
+ }
802
+ renderInstanceof(expr, exprTransformer) {
803
+ return `(typeid(*${this.render(expr.object, exprTransformer)}) == typeid(${expr.className}))`;
804
+ }
805
+ renderElementAccess(expr, exprTransformer, knownVariableTypes) {
806
+ const objectText = this.render(expr.object, exprTransformer);
807
+ // An enum-typed index (e.g. `GLYPHS[op]` where `op: Op`, a `const enum`)
808
+ // must be cast to an integral type — a C++ `enum class` does not
809
+ // implicitly convert to `size_t`, so `vector[enumValue]` fails to compile.
810
+ // `renderEnumSafeValue` already wraps numeric-enum operands in
811
+ // `static_cast<int>(...)` (SUPPORT_MATRIX §1.7); route the index through it
812
+ // (with the in-scope variable types so a bare enum-typed identifier
813
+ // resolves) so enum indices lower correctly. Non-enum indices are passed
814
+ // through unchanged. Demo #28 Finding E.
815
+ const indexText = this.renderEnumSafeValue(expr.index, knownVariableTypes);
816
+ return `${objectText}[${indexText}]`;
817
+ }
818
+ isEnumComparisonOperand(expr, inferredType) {
819
+ if (inferredType && this.enumNames.has(inferredType))
820
+ return true;
821
+ // Property/element access on an enum name (e.g. Color::Green) is also enum-valued.
822
+ if (expr.kind === "property-access" && expr.object.kind === "identifier" && this.enumNames.has(expr.object.value))
823
+ return true;
824
+ return false;
825
+ }
826
+ /**
827
+ * Render an expression value that will be assigned to a non-enum lvalue,
828
+ * passed as a non-enum argument, or otherwise consumed where C++ `enum class`
829
+ * won't implicitly convert. If the value is a numeric-enum operand, wrap it
830
+ * in `static_cast<int>(...)` so it composes with int-typed targets (e.g.
831
+ * `links[0] = LinkFlag.Wired` → `links[0] = static_cast<int>(LinkFlag::Wired)`).
832
+ * String-enum operands are passed through unchanged (they're `const char*`).
833
+ *
834
+ * This is the assignment/argument-site counterpart to the operator-level
835
+ * enum wrapping in renderBinary. SUPPORT_MATRIX §1.7: enum values used as
836
+ * integers need an explicit cast in C++.
837
+ */
838
+ renderEnumSafeValue(expr, knownVariableTypes) {
839
+ const rendered = this.render(expr, undefined, knownVariableTypes);
840
+ const inferredType = this.inferExpressionCppType(expr, knownVariableTypes);
841
+ if (!this.isStringEnumOperand(expr, inferredType) &&
842
+ this.isEnumComparisonOperand(expr, inferredType)) {
843
+ const enumName = this.getNumericEnumOperandName(expr, inferredType);
844
+ const castType = enumName
845
+ ? (this.strategy.enumCastType(enumName) ?? (this.largeEnumNames.has(enumName) ? "long" : "int"))
846
+ : "int";
847
+ if (/^static_cast<[^>]+>\(/.test(rendered)) {
848
+ return rendered;
849
+ }
850
+ return `static_cast<${castType}>(${rendered})`;
851
+ }
852
+ return rendered;
853
+ }
854
+ /**
855
+ * Render a value that will be stored into an lvalue of a known C++ type
856
+ * (an `assign` target, a `var_decl` initializer, a `push_back` argument,
857
+ * ...). Centralizes the **enum ↔ integral storage boundary** so every
858
+ * assignment/initialization site lowers consistently:
859
+ *
860
+ * - target is a numeric C++ type, value is a numeric-enum operand →
861
+ * `static_cast<int>(value)` (enum → int). This is the existing
862
+ * `renderEnumSafeValue` direction, now driven by the target type
863
+ * instead of duplicated per site.
864
+ * - target is a C++ `enum class`, value is a numeric/element-access
865
+ * operand → `static_cast<EnumType>(value)` (int → enum). C++ `enum
866
+ * class` does not implicitly convert FROM an integral storage type
867
+ * either, so reading an `int`/`uint8_t` cell back into an enum-typed
868
+ * local needs the reverse cast. SUPPORT_MATRIX §1.7.
869
+ *
870
+ * This closes the "enum↔integral storage boundary" family — the previous
871
+ * point-specific casts handled enum-as-array-index (demo #28 E) and
872
+ * enum-as-Map-key (demo #28 E review) but NOT enum stored into integral
873
+ * storage, integral storage read back into an enum, or an enum value
874
+ * passed to `push_back` on an integral-element vector. Demo #32 Finding A.
875
+ *
876
+ * String enums (`const char*`) and string-like targets are passed through
877
+ * unchanged. Unknown/auto target types are passed through unchanged
878
+ * (safer than a wrong cast).
879
+ */
880
+ renderValueForTarget(expr, targetType, knownVariableTypes, exprTransformer) {
881
+ const rendered = this.render(expr, exprTransformer, knownVariableTypes);
882
+ if (!targetType)
883
+ return rendered;
884
+ const normalizedTarget = targetType.trim();
885
+ // Never cast into string-like storage — enums that lower to const char*
886
+ // (string enums) already match, and a string target is never an enum boundary.
887
+ if (this.isStringLikeCppType(normalizedTarget))
888
+ return rendered;
889
+ const valueIsEnum = !this.isStringEnumOperand(expr, undefined)
890
+ && this.isEnumComparisonOperand(expr, this.inferExpressionCppType(expr, knownVariableTypes));
891
+ const targetIsEnum = this.enumNames.has(normalizedTarget) && !this.stringEnumNames.has(normalizedTarget);
892
+ const targetIsIntegral = INTEGRAL_CPP_TYPE_RE.test(normalizedTarget);
893
+ // enum value → integral storage: cast the value to int.
894
+ if (targetIsIntegral && valueIsEnum && !/^static_cast<[^>]+>\(/.test(rendered)) {
895
+ return this.renderEnumSafeValue(expr, knownVariableTypes);
896
+ }
897
+ // integral value → enum storage: cast the value to the enum type.
898
+ if (targetIsEnum && !valueIsEnum) {
899
+ const valueType = this.inferExpressionCppType(expr, knownVariableTypes);
900
+ const valueIsIntegral = valueType !== undefined && INTEGRAL_CPP_TYPE_RE.test(valueType);
901
+ if (valueIsIntegral && !/^static_cast<[^>]+>\(/.test(rendered)) {
902
+ return `static_cast<${normalizedTarget}>(${rendered})`;
903
+ }
904
+ }
905
+ return rendered;
906
+ }
907
+ /**
908
+ * Resolve a rendered C++ lvalue STRING (the `target` carried by an `assign`
909
+ * IR node, e.g. `this->nxt[r][c]`, `cells[i]`, `flag`) to its declared C++
910
+ * element/value type. This is the string-target counterpart to
911
+ * `inferExpressionCppType`'s element-access / property-access / identifier
912
+ * branches: the assign target is already rendered to text at IR-build time,
913
+ * so the emit layer must recover its type from the text + the shared type
914
+ * maps (the same maps `inferExpressionCppType` consults).
915
+ *
916
+ * Structural, not regex-rewrite: it splits the base from trailing `[...]`
917
+ * subscripts, resolves the base via the class-field / local / global-pointer
918
+ * maps, then unwraps one `std::vector<T>` / `StaticArray<T,N>` layer per
919
+ * subscript. Returns `undefined` when the type can't be resolved confidently
920
+ * — callers fall through to plain rendering (never an incorrect cast).
921
+ * Demo #32 Finding A.
922
+ */
923
+ inferLvalueCppType(target, knownVariableTypes) {
924
+ let depth = 0;
925
+ let base = target.trim();
926
+ // Strip trailing `[...]` subscripts (balanced: a subscript body holds no
927
+ // nested brackets in practice because element-access lowers one level at a
928
+ // time; a `this->m[a[b]]` would render `this->m[a[b]]` whose outermost
929
+ // `[a[b]]` we still strip correctly via the greedy-non-bracket regex below
930
+ // — the inner `[b]` is left on `a` and resolved as a separate base, which
931
+ // is fine because we only need the OUTER element type here).
932
+ while (/\[[^\[\]]*\]\s*$/.test(base)) {
933
+ base = base.replace(/\[[^\[\]]*\]\s*$/, "").trim();
934
+ depth++;
935
+ }
936
+ const baseType = this.resolveLvalueBaseType(base, knownVariableTypes);
937
+ if (!baseType)
938
+ return undefined;
939
+ if (depth === 0)
940
+ return baseType;
941
+ // Unwrap `depth` layers of std::vector<T> / StaticArray<T,N>.
942
+ let inner = baseType;
943
+ for (let i = 0; i < depth; i++) {
944
+ const parsed = parseCppType(inner);
945
+ if (parsed.kind === "vector" || parsed.kind === "staticArray" || parsed.kind === "cArray") {
946
+ inner = renderCppType(parsed.element);
947
+ }
948
+ else if (parsed.kind === "named" && parsed.name === "StaticArray" && parsed.args?.[0]) {
949
+ inner = renderCppType(parsed.args[0]);
950
+ }
951
+ else {
952
+ return undefined;
953
+ }
954
+ }
955
+ return inner;
956
+ }
957
+ /**
958
+ * Resolve a subscript-free rendered base (`this->field`, `obj->field`,
959
+ * `obj.field`, or a bare local name) to its declared C++ type. The string
960
+ * counterpart of `inferExpressionCppType`'s identifier / property-access
961
+ * branches. Demo #32 Finding A.
962
+ */
963
+ resolveLvalueBaseType(base, knownVariableTypes) {
964
+ const memberMatch = base.match(/^(.+?)(?:->|\.)(\w+)$/);
965
+ if (memberMatch) {
966
+ const [, objStr, propName] = memberMatch;
967
+ const objBase = objStr.trim();
968
+ // `this->field` / `(*this).field`: class fields are seeded into the
969
+ // known-variable-types map under their bare name (mirrors how
970
+ // inferExpressionCppType's property-access `this` branch resolves), so a
971
+ // `this->prop` lookup hits `knownVariableTypes.get(prop)`.
972
+ if (objBase === "this" || objBase === "(*this)") {
973
+ return knownVariableTypes?.get(propName)?.cppType;
974
+ }
975
+ // `obj->field` / `obj.field`: resolve the object's type, then look the
976
+ // field up in the interface-field-type map for that struct.
977
+ const objType = this.resolveLvalueBaseType(objBase, knownVariableTypes);
978
+ if (objType) {
979
+ const structName = this.normalizeRecordType(objType).replace(/\s*\*$/, "").trim();
980
+ const fieldType = this.interfaceFieldTypes.get(structName)?.get(propName);
981
+ if (fieldType)
982
+ return fieldType;
983
+ }
984
+ return undefined;
985
+ }
986
+ // Bare local / parameter / global.
987
+ if (/^\w+$/.test(base)) {
988
+ const local = knownVariableTypes?.get(base)?.cppType;
989
+ if (local)
990
+ return local;
991
+ const ptr = this.globalPointerVarTypes?.get(base);
992
+ if (ptr)
993
+ return ptr;
994
+ if (this.stringVarNames?.has(base))
995
+ return "std::string";
996
+ }
997
+ return undefined;
998
+ }
999
+ /**
1000
+ * Shared enum-operand wrapping for binary operators. Detects whether each
1001
+ * side is a numeric-enum operand (excluding string enums), casts it to int,
1002
+ * and applies defensive symmetry: if exactly one side is an enum, the other
1003
+ * side is also cast so the operator sees `int op int`. Returns the
1004
+ * (possibly rewrapped) left and right strings.
1005
+ *
1006
+ * Used by the arithmetic, bitwise, and comparison branches of renderBinary
1007
+ * so the enum-cast logic lives in one place rather than three.
1008
+ */
1009
+ castEnumOperandsForOperator(leftExpr, rightExpr, leftType, rightType, leftRendered, rightRendered) {
1010
+ const leftIsEnum = !this.isStringEnumOperand(leftExpr, leftType) && this.isEnumComparisonOperand(leftExpr, leftType);
1011
+ const rightIsEnum = !this.isStringEnumOperand(rightExpr, rightType) && this.isEnumComparisonOperand(rightExpr, rightType);
1012
+ let left = leftRendered;
1013
+ let right = rightRendered;
1014
+ if (leftIsEnum) {
1015
+ left = `static_cast<int>(${leftRendered})`;
1016
+ }
1017
+ if (rightIsEnum) {
1018
+ right = `static_cast<int>(${rightRendered})`;
1019
+ }
1020
+ // Defensive symmetry: static_cast<int> is always safe on a scalar (enum,
1021
+ // int, double, bool all convert) and produces the matching-type `int op
1022
+ // int` form C++ requires when one side is enum and the other isn't.
1023
+ if (leftIsEnum && !rightIsEnum) {
1024
+ right = `static_cast<int>(${rightRendered})`;
1025
+ }
1026
+ if (rightIsEnum && !leftIsEnum) {
1027
+ left = `static_cast<int>(${leftRendered})`;
1028
+ }
1029
+ return { left, right, leftIsEnum, rightIsEnum };
1030
+ }
1031
+ /**
1032
+ * Returns true when an expression is a string-enum member access, i.e. it
1033
+ * resolves to a `constexpr const char*` (e.g. `Color.Red` where Color is a
1034
+ * TS string enum). Such operands must NOT be static_cast<int>'d in
1035
+ * comparisons — they compare as C-strings instead.
1036
+ */
1037
+ isStringEnumOperand(expr, inferredType) {
1038
+ if (inferredType && this.stringEnumNames.has(inferredType))
1039
+ return true;
1040
+ if (expr.kind === "property-access" && expr.object.kind === "identifier" && this.stringEnumNames.has(expr.object.value))
1041
+ return true;
1042
+ return false;
1043
+ }
1044
+ getNumericEnumOperandName(expr, inferredType) {
1045
+ if (inferredType && this.enumNames.has(inferredType) && !this.stringEnumNames.has(inferredType)) {
1046
+ return inferredType;
1047
+ }
1048
+ if (expr.kind === "property-access" &&
1049
+ expr.object.kind === "identifier" &&
1050
+ this.enumNames.has(expr.object.value) &&
1051
+ !this.stringEnumNames.has(expr.object.value)) {
1052
+ return expr.object.value;
1053
+ }
1054
+ return undefined;
1055
+ }
1056
+ renderBinary(expr, exprTransformer, knownVariableTypes) {
1057
+ const leftRendered = this.render(expr.left, exprTransformer, knownVariableTypes);
1058
+ const rightRendered = this.render(expr.right, exprTransformer, knownVariableTypes);
1059
+ // Platform-specific string concat wrapping (Arduino: String())
1060
+ if (expr.operator === "+") {
1061
+ const wrapped = this.strategy.wrapStringConcat(leftRendered, rightRendered, expr.left.kind === "string");
1062
+ if (wrapped !== undefined)
1063
+ return wrapped;
1064
+ }
1065
+ // C++ doesn't define % for double — use fmod
1066
+ const modLeftType = this.inferExpressionCppType(expr.left, knownVariableTypes);
1067
+ const modRightType = this.inferExpressionCppType(expr.right, knownVariableTypes);
1068
+ if (expr.operator === "%" && (modLeftType === "double" || modLeftType === "float" || modRightType === "double" || modRightType === "float")) {
1069
+ const left = expr.left.kind === "binary" ? `(${leftRendered})` : leftRendered;
1070
+ const right = expr.right.kind === "binary" ? `(${rightRendered})` : rightRendered;
1071
+ return `fmod(${left}, ${right})`;
1072
+ }
1073
+ // Promote integer division to double to match JavaScript semantics
1074
+ if (expr.operator === "/" && this.strategy.promoteDivisionToDouble?.()) {
1075
+ const left = expr.left.kind === "binary" ? `(${leftRendered})` : leftRendered;
1076
+ const right = expr.right.kind === "binary" ? `(${rightRendered})` : rightRendered;
1077
+ return `static_cast<double>(${left}) / static_cast<double>(${right})`;
1078
+ }
1079
+ if (expr.operator === "**") {
1080
+ const left = expr.left.kind === "binary" ? `(${leftRendered})` : leftRendered;
1081
+ const right = expr.right.kind === "binary" ? `(${rightRendered})` : rightRendered;
1082
+ return `pow(${left}, ${right})`;
1083
+ }
1084
+ // Wrap enum-class operands in static_cast<int>() for arithmetic operators
1085
+ // (+, -, *, /) and bitwise operators (&, |, ^, <<, >>). C++ enum class
1086
+ // values don't interoperate with int implicitly — `trophic - 1`, `a | b`,
1087
+ // `x & MASK` all fail without the cast. SUPPORT_MATRIX §1.7/§5.1. The
1088
+ // shared castEnumOperandsForOperator helper handles detection + defensive
1089
+ // symmetry in one place.
1090
+ const numericOps = new Set(["+", "-", "*", "/", "&", "|", "^", "<<", ">>"]);
1091
+ let preLeft = leftRendered;
1092
+ let preRight = rightRendered;
1093
+ if (numericOps.has(expr.operator)) {
1094
+ // For "+", only cast when neither side is string-like — "+" may be
1095
+ // string concatenation (handled by the wrapStringConcat path above),
1096
+ // and casting a string operand to int would be wrong. Numeric "+"
1097
+ // (e.g. `power + rarity`) still needs the enum-side cast. The other
1098
+ // numeric ops are never string concat, so they always qualify.
1099
+ const leftIsStringLike = this.isStringLikeCppType(modLeftType) || expr.left.kind === "string";
1100
+ const rightIsStringLike = this.isStringLikeCppType(modRightType) || expr.right.kind === "string";
1101
+ const isNumericContext = expr.operator !== "+" || (!leftIsStringLike && !rightIsStringLike);
1102
+ if (isNumericContext) {
1103
+ const cast = this.castEnumOperandsForOperator(expr.left, expr.right, modLeftType, modRightType, leftRendered, rightRendered);
1104
+ preLeft = cast.left;
1105
+ preRight = cast.right;
1106
+ }
1107
+ }
1108
+ // Wrap enum-class operands in static_cast<int>() for comparisons against
1109
+ // integers, since C++ enum class values don't compare with int implicitly.
1110
+ const comparisonOps = new Set(["==", "===", "!=", "!==", "<", "<=", ">", ">="]);
1111
+ let finalLeft = preLeft;
1112
+ let finalRight = preRight;
1113
+ if (comparisonOps.has(expr.operator)) {
1114
+ const leftIsStringEnum = this.isStringEnumOperand(expr.left, modLeftType);
1115
+ const rightIsStringEnum = this.isStringEnumOperand(expr.right, modRightType);
1116
+ const leftIsStrLiteral = expr.left.kind === "string";
1117
+ const rightIsStrLiteral = expr.right.kind === "string";
1118
+ // Detect operands that render to a raw C-string value (char buffer from
1119
+ // snprintf concat, or a string enum member) so equality against a string
1120
+ // literal uses strcmp instead of pointer ==.
1121
+ // Only snprintf buffers and string enums need this — std::string has
1122
+ // operator==, and const char* variables retain their pre-existing ==
1123
+ // behavior. Including std::string here would break valid comparisons.
1124
+ const isRawCString = (e, t) => {
1125
+ if (e.kind === "string_concat" || e.kind === "template_string")
1126
+ return true;
1127
+ if (e.kind === "string")
1128
+ return true;
1129
+ return this.isStringEnumOperand(e, t);
1130
+ };
1131
+ const leftIsCStringValue = isRawCString(expr.left, modLeftType);
1132
+ const rightIsCStringValue = isRawCString(expr.right, modRightType);
1133
+ // String equality: any C-string operand compared with == / != must use
1134
+ // strcmp (C++ pointer == on char* compares addresses, not contents).
1135
+ // But std::string/String variables have operator==, so skip strcmp when
1136
+ // an operand is a managed-string *variable* (would break valid ==).
1137
+ // Note: string_concat/template_string render to char[] buffers even
1138
+ // though their inferred type is std::string, so they still need strcmp.
1139
+ const isManagedStringVar = (e, t) => {
1140
+ if (e.kind === "string_concat" || e.kind === "template_string" || e.kind === "string")
1141
+ return false;
1142
+ return needsCStrForStringLike(this.strategy.normalizeCppType(t ?? ""));
1143
+ };
1144
+ if ((expr.operator === "===" || expr.operator === "==" || expr.operator === "!==" || expr.operator === "!=") &&
1145
+ (leftIsCStringValue || rightIsCStringValue) &&
1146
+ !isManagedStringVar(expr.left, modLeftType) && !isManagedStringVar(expr.right, modRightType)) {
1147
+ const wantEqual = expr.operator === "===" || expr.operator === "==";
1148
+ finalLeft = `strcmp(${leftRendered}, ${rightRendered})`;
1149
+ return `${finalLeft} ${wantEqual ? "==" : "!="} 0`;
1150
+ }
1151
+ // After the strcmp path, apply the shared enum-operand cast (with
1152
+ // defensive symmetry). The strcmp path above handles string-valued
1153
+ // operands; this handles numeric-enum operands compared against ints.
1154
+ const cast = this.castEnumOperandsForOperator(expr.left, expr.right, modLeftType, modRightType, leftRendered, rightRendered);
1155
+ finalLeft = cast.left;
1156
+ finalRight = cast.right;
1157
+ }
1158
+ // Precedence-aware parenthesization to preserve TS semantics in C++.
1159
+ const cppOp = expr.operator === "===" ? "==" : expr.operator === "!==" ? "!=" : expr.operator;
1160
+ const myPrec = operatorPrecedence(expr.operator);
1161
+ const leftNeedsParens = expr.left.kind === "binary" && operatorPrecedence(expr.left.operator) < myPrec;
1162
+ const rightNeedsParens = expr.right.kind === "binary" && operatorPrecedence(expr.right.operator) <= myPrec;
1163
+ const left = leftNeedsParens ? `(${finalLeft})` : finalLeft;
1164
+ const right = rightNeedsParens ? `(${finalRight})` : finalRight;
1165
+ return `${left} ${cppOp} ${right}`;
1166
+ }
1167
+ renderUnary(expr, exprTransformer) {
1168
+ const rendered = this.render(expr.operand, exprTransformer);
1169
+ // Postfix operators (e.g. i++, i--) render after the operand.
1170
+ if (expr.postfix) {
1171
+ return `${rendered}${expr.operator}`;
1172
+ }
1173
+ return `${expr.operator}${rendered}`;
1174
+ }
1175
+ renderPropertyAccess(expr, exprTransformer, knownVariableTypes) {
1176
+ const chain = extractPropertyChain(expr);
1177
+ if (chain) {
1178
+ // Check for Board.definition.* access first
1179
+ const boardDef = this.strategy.renderBoardDefinitionAccess(chain, this.boardConstants);
1180
+ if (boardDef !== undefined)
1181
+ return boardDef;
1182
+ // Check for peripheral stub property access (I2C0.isInitialized, SPI0.isInitialized, Serial.isInitialized)
1183
+ const peripheralProperty = renderPeripheralProperty(chain, this.strategy);
1184
+ if (peripheralProperty !== undefined)
1185
+ return peripheralProperty;
1186
+ }
1187
+ const objStr = this.render(expr.object, exprTransformer);
1188
+ // In C++, 'this' is a pointer — always use -> for member access.
1189
+ // But first: if `this` has a getter for the accessed property, rewrite
1190
+ // to a getter call (`this->getX()`). The class emitter registers the
1191
+ // current class's accessors under the "this" key in varAccessorNames
1192
+ // while emitting each non-static method. This must run before the
1193
+ // plain `this->x` early-return below, otherwise getter access on
1194
+ // `this` is never rewritten (the call site stays as `this->x` and
1195
+ // fails to compile against the generated `getX()` method).
1196
+ if (expr.object.kind === "raw" && expr.object.value === "this") {
1197
+ const thisAccessors = this.varAccessorNames.get("this");
1198
+ if (thisAccessors?.has(expr.property)) {
1199
+ const getterName = accessorGetterName(expr.property);
1200
+ return `this->${getterName}()`;
1201
+ }
1202
+ const safeProperty = escapeCppKeyword(expr.property, this.strategy.reservedNames());
1203
+ return `this->${safeProperty}`;
1204
+ }
1205
+ // Use C++ scope-resolution operator (::) for enum class member access.
1206
+ if (expr.isEnum || (expr.object.kind === "identifier" && this.enumNames.has(expr.object.value))) {
1207
+ const enumName = expr.object.kind === "identifier" ? expr.object.value : objStr;
1208
+ const enumMember = this.strategy.renameEnumMember(enumName, expr.property);
1209
+ const enumAccess = `${objStr}::${enumMember}`;
1210
+ const castType = this.strategy.enumCastType(enumName);
1211
+ if (castType !== undefined) {
1212
+ return `static_cast<${castType}>(${enumAccess})`;
1213
+ }
1214
+ return enumAccess;
1215
+ }
1216
+ // Use C++ scope-resolution operator (::) for namespace or static member access.
1217
+ // BUT: a top-level const object (e.g. `const CONFIG = {...}` lowered to an
1218
+ // `extern _CONFIG_t CONFIG` variable) is an instance, not a namespace/class,
1219
+ // even though the IR may flag it isStatic. Use `.` for those.
1220
+ if (expr.object.kind === "identifier" && this.knownTopLevelObjectTypes?.has(expr.object.value)) {
1221
+ const accessor = expr.isPointer ? "->" : ".";
1222
+ const safeProperty = escapeCppKeyword(expr.property, this.strategy.reservedNames());
1223
+ return `${objStr}${accessor}${safeProperty}`;
1224
+ }
1225
+ // Multi-level static access through a namespace: `Ns.Class.member`. The
1226
+ // outer `Ns.Class` renders via the namespace branch below as `Ns::Class`
1227
+ // (a namespace-qualified class path). The INNER `.member` access has an
1228
+ // `expr.object` that is itself a property-access (not an identifier), so
1229
+ // the `namespaceNames.has(...)` check below misses it — it fell through to
1230
+ // the instance `.` default and emitted `Ns::Class.member` (mixed), failing
1231
+ // at g++ time for a static member (needs `Ns::Class::member`). When the
1232
+ // object rendered to a `::`-form it names a namespace/class, not an
1233
+ // instance, so the member access is also `::`.
1234
+ if (objStr.includes("::") && expr.object.kind === "property-access") {
1235
+ const safeProperty = escapeCppKeyword(expr.property, this.strategy.reservedNames());
1236
+ return `${objStr}::${safeProperty}`;
1237
+ }
1238
+ if (expr.isNamespace || expr.isStatic || (expr.object.kind === "identifier" && this.namespaceNames.has(expr.object.value))) {
1239
+ // Static getter access: `Counter.total` where `total` is a static getter
1240
+ // lowers to `Counter::getTotal()`, not the raw field `Counter::total`.
1241
+ // (Instance getters are handled below; this branch runs first for static
1242
+ // access and would otherwise bypass the getter rewrite — demo #14 E.)
1243
+ if (expr.isStatic && expr.object.kind === "identifier") {
1244
+ const staticAccessors = this.typeAccessorNames.get(expr.object.value);
1245
+ if (staticAccessors?.has(expr.property)) {
1246
+ const getterName = accessorGetterName(expr.property);
1247
+ return `${objStr}::${getterName}()`;
1248
+ }
1249
+ }
1250
+ const safeProperty = escapeCppKeyword(expr.property, this.strategy.reservedNames());
1251
+ return `${objStr}::${safeProperty}`;
1252
+ }
1253
+ if (expr.object.kind === "identifier" && (expr.property === "length" || expr.property === "size")) {
1254
+ const varName = expr.object.value;
1255
+ if (this.cArrayVarNames?.has(varName)) {
1256
+ return `(sizeof(${objStr}) / sizeof(${objStr}[0]))`;
1257
+ }
1258
+ const varInfo = knownVariableTypes?.get(varName) ?? this.knownVariableTypes?.get(varName);
1259
+ if (varInfo && parseCppType(varInfo.cppType).kind === "staticArray") {
1260
+ return `${objStr}.${expr.property}()`;
1261
+ }
1262
+ // C-style strings (const char*, char*) require strlen().
1263
+ if (varInfo && (varInfo.cppType === "const char*" || varInfo.cppType === "char*")) {
1264
+ return `strlen(${objStr})`;
1265
+ }
1266
+ // std::vector<T> exposes .size() (not .length).
1267
+ if (varInfo && parsedIsVector(varInfo.cppType)) {
1268
+ return `${objStr}.size()`;
1269
+ }
1270
+ // String variable detected via IR scan (Issue 2): std::string exposes length()/size().
1271
+ if (this.stringVarNames?.has(varName)) {
1272
+ return `${objStr}.${expr.property}()`;
1273
+ }
1274
+ }
1275
+ // Passthrough enums: members render as bare identifiers (e.g. INTERNAL, not AnalogReference::INTERNAL).
1276
+ if (expr.object.kind === "identifier" && this.strategy.passthroughEnumNames?.().has(expr.object.value)) {
1277
+ return expr.property;
1278
+ }
1279
+ // Fallback for general size/length accessors if not handled above
1280
+ if (expr.property === "size" || expr.property === "length") {
1281
+ // (Handled above for identified strings/arrays)
1282
+ }
1283
+ // Rewrite property access to getter call if the property is an accessor.
1284
+ // Primary lookup: the receiver is an explicitly-registered variable
1285
+ // (var_decl / param / top-level binding) in varAccessorNames. Fallback:
1286
+ // the receiver's resolved C++ type names a class with getters
1287
+ // (typeAccessorNames). The fallback covers for-of loop variables, function
1288
+ // returns, and chained access — receivers whose variable name was never
1289
+ // registered but whose type is a known class with accessors.
1290
+ if (expr.object.kind === "identifier") {
1291
+ let accessors = this.varAccessorNames.get(expr.object.value);
1292
+ if (!accessors) {
1293
+ const resolvedType = this.inferExpressionCppType(expr.object, knownVariableTypes);
1294
+ if (resolvedType) {
1295
+ // Bare class name (pointer/const stripped) via structured IR.
1296
+ const bareTypeStr = renderCppType(bareType(parseCppType(resolvedType)));
1297
+ accessors = this.typeAccessorNames.get(bareTypeStr);
1298
+ }
1299
+ }
1300
+ if (accessors?.has(expr.property)) {
1301
+ const getterName = accessorGetterName(expr.property);
1302
+ return `${objStr}->${getterName}()`;
1303
+ }
1304
+ }
1305
+ // Decide -> vs "." for the member access. Prefer the IR's isPointer flag
1306
+ // (set during IR build for pointer variables / this). When that's absent,
1307
+ // fall back to resolving the object's C++ type: if it's a pointer (ends
1308
+ // with '*'), the access must use ->. This catches struct/interface fields
1309
+ // of class-pointer type (e.g. `dungeon.monster.name` where monster is a
1310
+ // Monster* field) that the IR-build-time isPointer detection misses,
1311
+ // because interface/struct field types aren't visible during IR build.
1312
+ let accessor = expr.isPointer ? "->" : ".";
1313
+ if (accessor === ".") {
1314
+ const objectType = this.inferExpressionCppType(expr.object, knownVariableTypes);
1315
+ if (objectType && parsedIsPointer(objectType)) {
1316
+ accessor = "->";
1317
+ }
1318
+ }
1319
+ const safeProperty = escapeCppKeyword(expr.property, this.strategy.reservedNames());
1320
+ const rendered = `${objStr}${accessor}${safeProperty}`;
1321
+ return rendered;
1322
+ }
1323
+ renderCallback(expr) {
1324
+ // Callbacks are rendered by the statement emitter which tracks them globally
1325
+ // Here we just return a marker that gets replaced with the actual function name
1326
+ return `/* callback:${expr.sourceSpan.startLine}:${expr.sourceSpan.startColumn} */`;
1327
+ }
1328
+ renderLambda(expr, exprTransformer) {
1329
+ const params = expr.params.map(p => `${p.cppType} ${p.name}`).join(", ");
1330
+ const ret = expr.returnType && expr.returnType !== "auto" ? ` -> ${expr.returnType}` : "";
1331
+ if (expr.isExpressionBody && expr.body.length === 1 && expr.body[0].kind === "return" && "value" in expr.body[0]) {
1332
+ return `[&](${params})${ret} { return ${this.render(expr.body[0].value, exprTransformer)}; }`;
1333
+ }
1334
+ const bodyStr = expr.body.map(s => {
1335
+ if (s.kind === "return" && s.value)
1336
+ return ` return ${this.render(s.value, exprTransformer)};`;
1337
+ if (s.kind === "var_decl") {
1338
+ const safeName = s.name;
1339
+ const init = s.initializer ? this.render(s.initializer, exprTransformer) : "";
1340
+ return ` auto ${safeName} = ${init};`;
1341
+ }
1342
+ if (s.kind === "assign")
1343
+ return ` ${s.target} ${s.operator} ${s.value ? this.render(s.value, exprTransformer) : ""};`;
1344
+ if (s.kind === "call")
1345
+ return ` ${this.render(s, exprTransformer)};`;
1346
+ return ` /* ${s.kind} */`;
1347
+ }).join("\n");
1348
+ return `[&](${params})${ret} {\n${bodyStr}\n}`;
1349
+ }
1350
+ renderMethodCall(expr, exprTransformer) {
1351
+ const argsText = expr.args.map(a => this.render(a, exprTransformer)).join(", ");
1352
+ let callee = exprTransformer ? exprTransformer(expr.callee) : expr.callee;
1353
+ // Convert . to :: for static or namespace method calls if the IR flagged them.
1354
+ if (expr.isStatic || expr.isNamespace) {
1355
+ callee = callee.replace(/\./g, "::");
1356
+ }
1357
+ const escapedStringVarNames = new Set(Array.from(this.stringVarNames ?? []).map(name => escapeCppKeyword(name, this.strategy.reservedNames())));
1358
+ const stringVarNames = this.stringVarNames ?? new Set();
1359
+ const cArrayNames = this.cArrayVarNames ?? new Set();
1360
+ // Convert . to -> for pointer method calls if the IR flagged them.
1361
+ if (expr.isPointer && !callee.includes("->")) {
1362
+ callee = callee.replace(/\./g, "->");
1363
+ }
1364
+ else if (!callee.includes("->")) {
1365
+ // Fallback: the IR's isPointer flag is unset when the receiver is a
1366
+ // module-scope (file-global) pointer variable accessed from a scope
1367
+ // where its type wasn't visible at IR build time — notably an
1368
+ // ISR-captured `const btn = new Button()` referenced inside the hoisted
1369
+ // callback. Consult globalPointerVarTypes (threaded from EmitterContext)
1370
+ // to recover the pointer-ness and arrow the leading `obj.` → `obj->`.
1371
+ // This is the structural replacement for the file-wide text sweep that
1372
+ // formerly lived in output-finalizer.ts. Only the leading receiver is
1373
+ // rewritten: a chain like `btn->field.method` keeps its inner `.`.
1374
+ const m = callee.match(/^([A-Za-z_$][\w$]*)\./);
1375
+ if (m && this.globalPointerVarTypes?.has(m[1])) {
1376
+ callee = callee.replace(/^([A-Za-z_$][\w$]*)\./, "$1->");
1377
+ }
1378
+ }
1379
+ if (/\b([A-Za-z_][A-Za-z0-9_]*)\.(?:length|size)$/g.test(callee)) {
1380
+ return callee;
1381
+ }
1382
+ callee = this.escapeFinalMemberName(callee);
1383
+ return `${callee}(${argsText})`;
1384
+ }
1385
+ escapeFinalMemberName(callee) {
1386
+ if (callee.startsWith("std::")) {
1387
+ return callee;
1388
+ }
1389
+ return callee.replace(/(->|::|\.)([A-Za-z_][A-Za-z0-9_]*)$/, (_match, separator, memberName) => {
1390
+ return `${separator}${escapeCppKeyword(memberName, this.strategy.reservedNames())}`;
1391
+ });
1392
+ }
1393
+ }
1394
+ /**
1395
+ * Returns the C++ operator precedence for precedence-aware parenthesization.
1396
+ * Higher number = higher precedence (binds tighter).
1397
+ */
1398
+ function operatorPrecedence(op) {
1399
+ switch (op) {
1400
+ case "**": return 6;
1401
+ case "*":
1402
+ case "/":
1403
+ case "%": return 5;
1404
+ case "+":
1405
+ case "-": return 4;
1406
+ case "<<":
1407
+ case ">>":
1408
+ case ">>>": return 3;
1409
+ case "<":
1410
+ case "<=":
1411
+ case ">":
1412
+ case ">=": return 2;
1413
+ case "==":
1414
+ case "!=": return 1;
1415
+ case "&": return 0;
1416
+ case "^": return -1;
1417
+ case "|": return -2;
1418
+ case "&&": return -3;
1419
+ case "||": return -4;
1420
+ default: return 0;
1421
+ }
1422
+ }
1423
+ /**
1424
+ * Transform class names in expressions to their fully qualified names.
1425
+ * E.g., "new SHT3x()" -> "new Microfire::SHT3x()"
1426
+ */
1427
+ function transformClassNames(value, classNameMap) {
1428
+ if (classNameMap.size === 0) {
1429
+ return value;
1430
+ }
1431
+ // Transform "new ClassName(" patterns
1432
+ let result = value;
1433
+ for (const [simpleName, fullName] of classNameMap) {
1434
+ // Only transform if the full name is different (has namespace)
1435
+ if (fullName !== simpleName && fullName.includes("::")) {
1436
+ const pattern = new RegExp(`\\bnew\\s+${simpleName}\\b`, "g");
1437
+ result = result.replace(pattern, `new ${fullName}`);
1438
+ }
1439
+ }
1440
+ return result;
1441
+ }
1442
+ /**
1443
+ * Normalizes a raw expression string for C++ output.
1444
+ * Handles === to == conversion, enum access, and class name transformation.
1445
+ */
1446
+ export function normalizeRawExpression(value, strategy, classNameMap) {
1447
+ let normalized = value
1448
+ .replace(/\?\?/g, "/* ?? */"); // Fallback for raw expressions; usually handled at IR level
1449
+ // Apply platform-specific expression normalisation
1450
+ normalized = strategy.normalizeRawExpression(normalized);
1451
+ // Transform Arduino library class names to their fully qualified names
1452
+ if (classNameMap) {
1453
+ normalized = transformClassNames(normalized, classNameMap);
1454
+ }
1455
+ return normalized;
1456
+ }
1457
+ /**
1458
+ * Transform type names to their fully qualified names (with namespaces).
1459
+ * E.g., "SHT3x*" -> "Microfire::SHT3x*", "SHT3x" -> "Microfire::SHT3x"
1460
+ */
1461
+ export function transformTypeName(cppType, classNameMap) {
1462
+ if (!classNameMap || classNameMap.size === 0) {
1463
+ return cppType;
1464
+ }
1465
+ let result = cppType;
1466
+ for (const [simpleName, fullName] of classNameMap) {
1467
+ // Only transform if the full name is different (has namespace)
1468
+ if (fullName !== simpleName && fullName.includes("::")) {
1469
+ // Match the simple name as a word boundary (not already qualified with ::)
1470
+ // Handle patterns like "SHT3x", "SHT3x*", "SHT3x&", etc.
1471
+ // Use negative lookbehind to avoid matching already-qualified names
1472
+ const pattern = new RegExp(`(?<![a-zA-Z0-9_:])${simpleName}\\b`, "g");
1473
+ result = result.replace(pattern, fullName);
1474
+ }
1475
+ }
1476
+ return result;
1477
+ }