@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,2249 @@
1
+ import ts from "typescript";
2
+ import { makeDiagnostic, makeSourceSpan } from "./ast-node-utils.js";
3
+ import { PIN_FACTORY_FUNCTIONS, CONSTANT_FOLD_FUNCTIONS, TYPED_ARRAY_ELEMENT_MAP, activeCArrayVars, activeArrayLiteralVars, activeStringVars, nestedFunctionAliases, nestedClassAliases, hoistedNestedClasses, mutableArrayVars, arrayLiteralSizes, filteredArrayLengthVars, activeNamespaceNames, activeEnumNames, activeStringEnumNames, topLevelClassNames, topLevelInterfaceNames, classTypeNames, topLevelClasses, getActiveExtendsClass, restParamFunctions, getContext, getCurrentBoardConstants } from "./build-ir-state.js";
4
+ import { getCurrentIrTypeScope } from "./symbol-types.js";
5
+ import { renderExprAsText } from "./render-expr.js";
6
+ import { lowerStatement, tryResolveHALExpression } from "./statement-to-ir.js";
7
+ import { isSignalName, resolveElementValue } from "./transformers/ui-call-resolver.js";
8
+ import { getCanvasAmbientCtx } from "./transformers/ui-callback-lowering.js";
9
+ import { halInstances } from "./hal-resolver.js";
10
+ import { escapeCppKeyword } from "../utils/strings.js";
11
+ import { tryLowerRegisterRead } from "./transformers/register-assignment.js";
12
+ import { tryLowerArrayAndStringMethods } from "./transformers/array-methods.js";
13
+ import { collectReturns, inferExprCppType, typeNodeToCppType } from "./type-resolution.js";
14
+ import { parseCppType, elementOf, renderCppType, bareType, parsedIsPointer, parsedIsVector, parsedIsMap, parsedIsSet, parsedIsTuple, parsedIsStdString, parsedElementString, parsedBareString, isContainer } from "../api/shared/cpp-type-ir.js";
15
+ function resolveExprCppType(expr) {
16
+ if (ts.isNonNullExpression(expr) || ts.isParenthesizedExpression(expr)) {
17
+ return resolveExprCppType(expr.expression);
18
+ }
19
+ if (ts.isIdentifier(expr)) {
20
+ const scope = getCurrentIrTypeScope();
21
+ const t = scope?.locals.get(expr.text) ?? scope?.globals.get(expr.text);
22
+ return t && t !== "auto" ? t : undefined;
23
+ }
24
+ if (ts.isPropertyAccessExpression(expr)) {
25
+ if (expr.expression.kind === ts.SyntaxKind.ThisKeyword) {
26
+ const fieldKey = `this->${expr.name.text}`;
27
+ const scope = getCurrentIrTypeScope();
28
+ const fieldType = scope?.classFields.get(fieldKey) ?? scope?.locals.get(fieldKey);
29
+ return fieldType && fieldType !== "auto" ? fieldType : undefined;
30
+ }
31
+ const receiverType = resolveExprCppType(expr.expression);
32
+ if (!receiverType)
33
+ return undefined;
34
+ const className = receiverType.replace(/\*$/, "");
35
+ const classDef = topLevelClasses.get(className);
36
+ if (!classDef)
37
+ return undefined;
38
+ const field = classDef.fields.find(f => f.name === expr.name.text);
39
+ return field ? field.cppType : undefined;
40
+ }
41
+ if (ts.isElementAccessExpression(expr)) {
42
+ const containerType = resolveExprCppType(expr.expression);
43
+ if (!containerType)
44
+ return undefined;
45
+ // One structured elementOf lookup replaces the vector / __tc_StaticArray
46
+ // inline parsers (the latter was a hand-rolled depth counter for the
47
+ // first template arg).
48
+ const elemIr = elementOf(parseCppType(containerType));
49
+ if (elemIr)
50
+ return renderCppType(elemIr);
51
+ }
52
+ if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) {
53
+ const receiver = expr.expression.expression;
54
+ const methodName = expr.expression.name.text;
55
+ if (ts.isIdentifier(receiver) && topLevelClasses.has(receiver.text)) {
56
+ return topLevelClasses.get(receiver.text)?.methods.find((method) => method.name === methodName)?.returnType;
57
+ }
58
+ const receiverType = resolveExprCppType(receiver);
59
+ if (!receiverType)
60
+ return undefined;
61
+ // Peel pointer via structured bareType to recover the class name.
62
+ const className = (() => {
63
+ const bare = bareType(parseCppType(receiverType));
64
+ return bare.kind === "named" ? bare.name : "";
65
+ })();
66
+ return topLevelClasses.get(className)?.methods.find((method) => method.name === methodName)?.returnType;
67
+ }
68
+ return undefined;
69
+ }
70
+ /**
71
+ * Infer the C++ element type of an array literal from its first element's
72
+ * resolved cppType. Used so an INLINE array literal that flows into a
73
+ * `__tc_*` template helper (`[...].join(sep)`, `[...].concat(x)`, ...) renders
74
+ * as a TYPED `std::vector<ElemType>{...}` — a bare brace-init-list cannot drive
75
+ * template argument deduction, but a typed temporary can.
76
+ *
77
+ * Returns the resolved cppType of the first element (e.g. `"std::string"` for
78
+ * `['a', 'b']`, `"int32_t"` for `[1, 2]`), or `"auto"` if no element carries a
79
+ * resolvable type. The `"auto"` fallback preserves the historical behavior for
80
+ * direct-initialization contexts (`const T x = {...}`), where a bare brace list
81
+ * is correct. Demo #29 Finding D.
82
+ */
83
+ function inferArrayElementType(tsElements) {
84
+ for (const elem of tsElements) {
85
+ const t = resolveExprCppType(elem);
86
+ if (t && t !== "auto") {
87
+ // A typed element (identifier/member-access/element-access) carries its
88
+ // declared cppType. Use it directly.
89
+ return t;
90
+ }
91
+ // Literal elements: resolve their cppType from the literal kind so a
92
+ // numeric/string/boolean literal array also gets a typed vector.
93
+ if (ts.isStringLiteral(elem) || ts.isNoSubstitutionTemplateLiteral(elem)) {
94
+ return "std::string";
95
+ }
96
+ if (ts.isNumericLiteral(elem)) {
97
+ // Float-looking literal → double, else int. Mirrors inferNumericCppType.
98
+ return /[.eE]/.test(elem.text) ? "double" : "int";
99
+ }
100
+ if (elem.kind === ts.SyntaxKind.TrueKeyword || elem.kind === ts.SyntaxKind.FalseKeyword) {
101
+ return "bool";
102
+ }
103
+ }
104
+ return "auto";
105
+ }
106
+ /**
107
+ * Math.* constant property accesses that lower to a numeric literal rather
108
+ * than `std::<name>` (which doesn't exist — `std::` has no PI/E members).
109
+ * Using literals avoids `<cmath>`/`M_PI` `_USE_MATH_DEFINES` portability
110
+ * issues on Windows/MSVC. Mirrors the TS `Math` constant values.
111
+ */
112
+ const MATH_CONSTANT_LITERALS = {
113
+ PI: 3.141592653589793,
114
+ E: 2.718281828459045,
115
+ LN2: 0.6931471805599453,
116
+ LN10: 2.302585092994046,
117
+ LOG2E: 1.4426950408889634,
118
+ LOG10E: 0.4342944819032518,
119
+ SQRT2: 1.4142135623730951,
120
+ SQRT1_2: 0.7071067811865476,
121
+ };
122
+ /**
123
+ * Resolve the declared C++ return type of a call expression, for the sole
124
+ * purpose of the `valueType === null` null-comparison guard. Returns the type
125
+ * (null-stripped, e.g. `Account` for `Account | null`) or `undefined`.
126
+ *
127
+ * Handles the two inline-call forms the guard needs:
128
+ * - `freeFn(args)` — a top-level `function` declaration's annotated return
129
+ * type, looked up from the source file (the IR `functionReturnTypes` map is
130
+ * not reachable here).
131
+ * - `this.method(args)` / `expr.method(args)` — a class method's return type,
132
+ * resolved via the top-level class registry (methods carry `returnType`).
133
+ *
134
+ * Demo #18 Finding A: without this, `findAccount(1) === null` and
135
+ * `this.find(1) === null` lower to the invalid `call() == CUTTLEFISH_UNDEFINED`.
136
+ */
137
+ function resolveCallReturnTypeForNullGuard(call, sourceText) {
138
+ const callee = call.expression;
139
+ // `this.method(...)` or `someExpr.method(...)`.
140
+ if (ts.isPropertyAccessExpression(callee)) {
141
+ const methodName = callee.name.text;
142
+ // Resolve the receiver class. `this` could be any top-level class; look up
143
+ // the method name across all of them (method names are usually unique
144
+ // within a file). For an identifier receiver, infer its class from the
145
+ // active type maps.
146
+ let candidateClassNames = [];
147
+ if (callee.expression.kind === ts.SyntaxKind.ThisKeyword) {
148
+ candidateClassNames = Array.from(topLevelClassNames);
149
+ }
150
+ else if (ts.isIdentifier(callee.expression)) {
151
+ const recvType = getCurrentIrTypeScope()?.locals.get(callee.expression.text) ?? getCurrentIrTypeScope()?.globals.get(callee.expression.text);
152
+ if (recvType)
153
+ candidateClassNames = [recvType.replace(/\*$/, "").replace(/^const\s+/, "")];
154
+ }
155
+ for (const className of candidateClassNames) {
156
+ const classDef = topLevelClasses.get(className);
157
+ if (classDef) {
158
+ const method = classDef.methods.find(m => m.name === methodName);
159
+ if (method)
160
+ return method.returnType;
161
+ }
162
+ }
163
+ return undefined;
164
+ }
165
+ // `freeFn(...)` — scan the source file for the function declaration.
166
+ if (ts.isIdentifier(callee)) {
167
+ const fnName = callee.text;
168
+ const sourceFile = call.getSourceFile();
169
+ let declaredReturnType;
170
+ const visit = (node) => {
171
+ if (declaredReturnType)
172
+ return;
173
+ if (ts.isFunctionDeclaration(node) && node.name?.text === fnName) {
174
+ declaredReturnType = node.type;
175
+ return;
176
+ }
177
+ ts.forEachChild(node, visit);
178
+ };
179
+ ts.forEachChild(sourceFile, visit);
180
+ if (declaredReturnType) {
181
+ return typeNodeToCppType(declaredReturnType);
182
+ }
183
+ return undefined;
184
+ }
185
+ return undefined;
186
+ }
187
+ export function expressionToIR(expr, sourceText, diagnostics, pointerVars = new Map()) {
188
+ function emitUnsupportedExpression(message) {
189
+ diagnostics.push(makeDiagnostic(sourceText, expr.pos, message, "error", "TS2CPP_UNSUPPORTED_EXPR"));
190
+ return { kind: "raw", value: "0 /* unsupported_expr */" };
191
+ }
192
+ function isOptionalChainNode(node) {
193
+ return !!node.questionDotToken ||
194
+ (typeof ts.isOptionalChain === "function" && ts.isOptionalChain(node));
195
+ }
196
+ /**
197
+ * Resolve .length property access to the correct C++ expression.
198
+ * Shared by both renderMemberAccessText and the main PropertyAccess handler
199
+ * to ensure consistent behavior for top-level and nested .length access.
200
+ *
201
+ * Returns the C++ text. Special case: returns the filteredArrayLengthVars
202
+ * variable name prefixed with "__FILTERED_LEN__" so the caller can detect it.
203
+ */
204
+ function resolveLengthProperty(receiverNode, objectText) {
205
+ if (ts.isStringLiteral(receiverNode) || ts.isNoSubstitutionTemplateLiteral(receiverNode)) {
206
+ return `${receiverNode.text.length}`;
207
+ }
208
+ // Escape C++ keywords in identifier texts for generated C++ output
209
+ const safeText = ts.isIdentifier(receiverNode) ? escapeCppKeyword(objectText) : objectText;
210
+ if (ts.isIdentifier(receiverNode) && filteredArrayLengthVars.has(receiverNode.text)) {
211
+ return `__FILTERED_LEN__${filteredArrayLengthVars.get(receiverNode.text)}`;
212
+ }
213
+ // Demo #17 Finding C — activeCArrayVars is FUNCTION-SCOPED (cleared by
214
+ // resetFunctionScopeState per function), so it is the authoritative signal
215
+ // for THIS function's typed-array / raw-C-array locals. mutableArrayVars,
216
+ // by contrast, is FILE-SCOPED (populated by a pre-scan and survives across
217
+ // functions), so a name like `buf` that was mutated in fn1-fn4 stays in
218
+ // mutableArrayVars when a DIFFERENT `buf` (a `new Uint8Array([...])`
219
+ // literal) is declared in fn5. Checking activeCArrayVars FIRST prevents the
220
+ // stale file-level mutableArrayVars entry from forcing the function-local
221
+ // raw C array down the `.size()` path (avr-g++: "request for member 'size'
222
+ // in 'buf', which is of non-class type 'uint8_t [5]'").
223
+ if (ts.isIdentifier(receiverNode) && activeCArrayVars.has(receiverNode.text)) {
224
+ return `(sizeof(${safeText}) / sizeof(${safeText}[0]))`;
225
+ }
226
+ if (ts.isIdentifier(receiverNode) && mutableArrayVars.has(receiverNode.text)) {
227
+ // std::vector has no `.length()` member (that's std::string); use
228
+ // `.size()`. Cast to long long to match loop-counter type and avoid
229
+ // -Wsign-compare. Demo #27 Finding C — this branch (function-local
230
+ // mutable array) and the array-literal branch below previously emitted
231
+ // the invalid `vector.length()`. The `this->field` path was already
232
+ // corrected by demo #22 fix F; this closes the bare-local-identifier
233
+ // hole.
234
+ return `static_cast<long long>(${safeText}.size())`;
235
+ }
236
+ if (ts.isIdentifier(receiverNode) && activeArrayLiteralVars.has(receiverNode.text)) {
237
+ const varName = receiverNode.text;
238
+ const varType = getCurrentIrTypeScope()?.locals.get(varName);
239
+ // `.size()` is valid ONLY when the local actually lowered to something
240
+ // with a `.size()` METHOD — a std::vector OR a promoted
241
+ // __tc_StaticArray. A NON-mutated local array literal on a target that
242
+ // does NOT need std::vector (`!strategy.needsStdVector()`, e.g. Arduino
243
+ // AVR) lowers to a RAW C array (`T name[] = {...}`), even though its
244
+ // varType still resolves to `std::vector<...>`, and a raw C array has NO
245
+ // `.size()` member. The emit-side discriminator (class-emitter.ts
246
+ // `addCArrayIfNotMutable`) uses exactly `!needsStdVector()` to decide
247
+ // raw-C-array vs std::vector, so we mirror it here. Previously this
248
+ // branch tested only the varType prefix, so a read-only local literal
249
+ // emitted `name.size()` on a raw C array → avr-g++ "request for member
250
+ // 'size' in 'name', which is of non-class type". `mutableArrayVars`
251
+ // membership is the second signal: a mutated local is promoted to
252
+ // __tc_StaticArray (variables.ts), which DOES have `.size()`. Demo #33.
253
+ const emitsRawCArray = !getContext().activeStrategy?.needsStdVector();
254
+ const loweredToContainer = (typeof varType === 'string' && varType.startsWith('StaticArray<')) ||
255
+ (typeof varType === 'string' && varType.startsWith('std::vector<') && (!emitsRawCArray || mutableArrayVars.has(varName)));
256
+ if (loweredToContainer) {
257
+ // Demo #27 Finding C — `.size()` not `.length()` on a vector/StaticArray
258
+ // local (see the mutableArrayVars comment above).
259
+ return `static_cast<long long>(${safeText}.size())`;
260
+ }
261
+ return `(sizeof(${safeText}) / sizeof(${safeText}[0]))`;
262
+ }
263
+ if (ts.isIdentifier(receiverNode) && getCurrentIrTypeScope()?.locals.get(receiverNode.text) === "auto") {
264
+ return `(sizeof(${safeText}) / sizeof(${safeText}[0]))`;
265
+ }
266
+ if (ts.isCallExpression(receiverNode)) {
267
+ const returnType = ts.isPropertyAccessExpression(receiverNode.expression) && ts.isIdentifier(receiverNode.expression.expression)
268
+ ? getCurrentIrTypeScope()?.locals.get(receiverNode.expression.expression.text)
269
+ : undefined;
270
+ if (returnType === "std::string")
271
+ return `static_cast<long long>(${safeText}.length())`;
272
+ // Cast .size() to long long to match the loop-counter type (TS number ->
273
+ // long long). Without this, `i < vec.size()` compares long long vs
274
+ // size_t (unsigned) and g++ -Wall warns -Wsign-compare on every
275
+ // indexed loop over an array.
276
+ return `static_cast<long long>(${safeText}.size())`;
277
+ }
278
+ // Resolve by concrete cppType first so std::string vars render member calls
279
+ // even when their resolved type is const char* (string-literal initialized).
280
+ if (ts.isIdentifier(receiverNode)) {
281
+ // locals holds function-scoped bindings; globals holds top-level
282
+ // (module-scope) bindings, which survive resetFunctionScopeState. A
283
+ // top-level `const S: int32_t[] = [...]` accessed inside a function is
284
+ // only in globals, so fall back to it.
285
+ const varType = getCurrentIrTypeScope()?.locals.get(receiverNode.text)
286
+ ?? getCurrentIrTypeScope()?.globals.get(receiverNode.text);
287
+ // Demo #17 Finding B — a typed-array local (`new Uint8Array([...])` /
288
+ // `new Int8Array(N)`) lowers to a RAW C array whose cppType is a static
289
+ // array spelling (`uint8_t[5]`, `int32_t[8]`) OR a bare element pointer
290
+ // (`uint8_t*`). Both forms have NO `.size()` member, so `.length` must
291
+ // lower to sizeof. The activeCArrayVars set is the canonical signal
292
+ // (populated by the variable transformer for `new <TypedArray>(...)`);
293
+ // the cppType shape check is a belt-and-braces fallback for cases where
294
+ // registration was missed (e.g. initializer took a non-standard path).
295
+ // Without this, `.length` fell through to the default `.size()` and
296
+ // avr-g++ rejected it ("request for member 'size' in 'buf', which is of
297
+ // non-class type 'uint8_t [5]'").
298
+ const isRawCArrayType = typeof varType === 'string'
299
+ && /\]\s*$/.test(varType); // ends with `[N]` or `[]`
300
+ if (activeCArrayVars.has(receiverNode.text) || isRawCArrayType) {
301
+ return `(sizeof(${safeText}) / sizeof(${safeText}[0]))`;
302
+ }
303
+ if (varType === "std::string") {
304
+ // Demo #30 Finding B — cast `std::string::length()` to `long long` so
305
+ // it matches the array/vector `.size()` lowering (also `long long`)
306
+ // AND the snprintf `%lld` format specifier. `std::string::length()`
307
+ // returns `size_type` (unsigned), which g++ -Wformat= rejects against
308
+ // `%d`/`%lld` and against `static_cast<long long>` only when NOT cast.
309
+ // Casting here makes `.length` uniform across every string/array/
310
+ // container receiver — one signed integral type, one format specifier.
311
+ return `static_cast<long long>(${safeText}.length())`;
312
+ }
313
+ // Demo #33 Finding B — a top-level `const` array literal on a target
314
+ // that does NOT need std::vector (`!strategy.needsStdVector()`, e.g.
315
+ // Arduino AVR) emits as a RAW C array (`int32_t S[] = {...}`), even
316
+ // though its declared type resolves to `std::vector<...>`. It is NOT in
317
+ // `mutableArrayVars` (only mutated locals promote to StaticArray) NOR
318
+ // in `activeArrayLiteralVars` (which resetFunctionScopeState clears
319
+ // before each function, so a top-level array isn't visible from inside
320
+ // a function body). So `.length` fell through to the default `.size()`
321
+ // — invalid for a raw C array (avr-g++: "request for member 'size' in
322
+ // 'S', which is of non-class type"). The emit-side discriminator
323
+ // (class-emitter.ts `addCArrayIfNotMutable`) uses `!needsStdVector()`,
324
+ // mirrored here: a top-level const array on a no-std::vector target is
325
+ // a raw C array → sizeof. On native/generic (`needsStdVector()` true)
326
+ // it really is a std::vector → .size(). The function-local path is
327
+ // handled by the activeArrayLiteralVars branch above (FIX-4).
328
+ if (typeof varType === 'string' && varType.startsWith('std::vector<')) {
329
+ const emitsRawCArray = !getContext().activeStrategy?.needsStdVector();
330
+ const isTopLevel = !getCurrentIrTypeScope()?.locals.has(receiverNode.text)
331
+ && getCurrentIrTypeScope()?.globals.has(receiverNode.text);
332
+ if (emitsRawCArray && isTopLevel) {
333
+ return `(sizeof(${safeText}) / sizeof(${safeText}[0]))`;
334
+ }
335
+ return `static_cast<long long>(${safeText}.size())`;
336
+ }
337
+ if (varType === "const char*" || varType === "char*") {
338
+ return `strlen(${safeText})`;
339
+ }
340
+ // Unresolved C-string variables (resolved const char*/char*/__tc_str_ptr) → strlen()
341
+ if (activeStringVars.has(receiverNode.text)) {
342
+ return `strlen(${safeText})`;
343
+ }
344
+ }
345
+ // Handle this->field.length — route by the field's resolved C++ type. Every
346
+ // STL container exposes .size(), so vector/map/set all lower correctly; only
347
+ // a C-string field lowers to strlen. Previously a std::map/std::set field
348
+ // fell through to the strlen default (the field type didn't match the
349
+ // std::vector/StaticArray prefix), emitting strlen(this->m) on a struct —
350
+ // invalid C++ that g++ rejects (or, worse, silently miscompiles). Demo #22
351
+ // adjacency probe surfaced this pre-existing bug.
352
+ if (ts.isPropertyAccessExpression(receiverNode) && receiverNode.expression.kind === ts.SyntaxKind.ThisKeyword) {
353
+ const fieldType = getCurrentIrTypeScope()?.locals.get(`this->${receiverNode.name.text}`);
354
+ if (fieldType === "std::string")
355
+ return `static_cast<long long>(${safeText}.length())`;
356
+ if (fieldType === "const char*" || fieldType === "char*")
357
+ return `strlen(${safeText})`;
358
+ // Container-like field (.size() applies). Covers std::vector, std::map,
359
+ // std::set, and both __tc_StaticArray<T,N> and the bare StaticArray<T,N>
360
+ // spelling (the latter parses as a named template, so check explicitly).
361
+ if (fieldType) {
362
+ const ir = parseCppType(fieldType);
363
+ const isBareStaticArray = ir.kind === "named" && ir.name === "StaticArray";
364
+ if (isContainer(ir) || isBareStaticArray) {
365
+ return `static_cast<long long>(${safeText}.size())`;
366
+ }
367
+ }
368
+ // Unknown field type — .size() is valid on every STL container and on
369
+ // std::string, so default to it rather than the C-string strlen (which is
370
+ // only correct for const char*). This matches the bare-identifier
371
+ // fallback at the end of this function.
372
+ return `static_cast<long long>(${safeText}.size())`;
373
+ }
374
+ return `static_cast<long long>(${safeText}.size())`;
375
+ }
376
+ function renderMemberAccessText(receiverNode, memberName) {
377
+ const escapedName = escapeCppKeyword(memberName);
378
+ const isThisAccess = receiverNode.kind === ts.SyntaxKind.ThisKeyword ||
379
+ (ts.isIdentifier(receiverNode) && receiverNode.text === "this");
380
+ if (isThisAccess) {
381
+ if (memberName === "length") {
382
+ return `static_cast<long long>(this->size())`;
383
+ }
384
+ return `this->${escapedName}`;
385
+ }
386
+ if (ts.isIdentifier(receiverNode) && pointerVars.has(receiverNode.text)) {
387
+ return `${receiverNode.text}->${escapedName}`;
388
+ }
389
+ if (ts.isIdentifier(receiverNode) && receiverNode.text === "Math") {
390
+ if (memberName === "random")
391
+ return `__tc_random()`;
392
+ const mathConst = MATH_CONSTANT_LITERALS[memberName];
393
+ if (mathConst !== undefined)
394
+ return String(mathConst);
395
+ return `std::${escapedName}`;
396
+ }
397
+ if (ts.isIdentifier(receiverNode) && activeNamespaceNames.has(receiverNode.text)) {
398
+ return `${receiverNode.text}::${escapedName}`;
399
+ }
400
+ const objectText = formatExpressionText(receiverNode);
401
+ // Detect if receiver is a method call that returns a pointer (for chaining)
402
+ if (ts.isCallExpression(receiverNode) && ts.isPropertyAccessExpression(receiverNode.expression)) {
403
+ const innerReceiver = receiverNode.expression.expression;
404
+ const innerMethodName = receiverNode.expression.name.text;
405
+ if (ts.isIdentifier(innerReceiver) && pointerVars.has(innerReceiver.text)) {
406
+ let className = pointerVars.get(innerReceiver.text);
407
+ className = nestedClassAliases.get(className) ?? className;
408
+ const cls = hoistedNestedClasses.find(c => c.name === className);
409
+ const chainMethod = cls?.methods.find(m => m.name === innerMethodName);
410
+ if (chainMethod && parsedIsPointer(chainMethod.returnType)) {
411
+ return `${objectText}->${escapedName}`;
412
+ }
413
+ }
414
+ }
415
+ if (memberName === "length" || memberName === "size") {
416
+ const resolved = resolveLengthProperty(receiverNode, objectText);
417
+ // Filtered length vars use a special prefix — extract the variable name
418
+ if (resolved.startsWith("__FILTERED_LEN__")) {
419
+ return resolved.slice("__FILTERED_LEN__".length);
420
+ }
421
+ return resolved;
422
+ }
423
+ return `${objectText}.${escapedName}`;
424
+ }
425
+ function renderOptionalGuardedAccess(receiverNode, accessText) {
426
+ const receiverText = formatExpressionText(receiverNode);
427
+ return `(cuttlefish_exists(${receiverText}) ? ${accessText} : 0)`;
428
+ }
429
+ const formatExpressionText = (node) => {
430
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
431
+ return formatExpressionText(node.expression);
432
+ }
433
+ if (ts.isParenthesizedExpression(node)) {
434
+ return `(${formatExpressionText(node.expression)})`;
435
+ }
436
+ if (ts.isPropertyAccessExpression(node)) {
437
+ const accessText = renderMemberAccessText(node.expression, node.name.text);
438
+ if (isOptionalChainNode(node)) {
439
+ return renderOptionalGuardedAccess(node.expression, accessText);
440
+ }
441
+ return accessText;
442
+ }
443
+ if (ts.isElementAccessExpression(node)) {
444
+ const objectText = formatExpressionText(node.expression);
445
+ const argumentText = node.argumentExpression ? formatExpressionText(node.argumentExpression) : "0";
446
+ return `${objectText}[${argumentText}]`;
447
+ }
448
+ if (ts.isCallExpression(node)) {
449
+ const argsText = node.arguments.map((arg) => formatExpressionText(arg)).join(", ");
450
+ if (isOptionalChainNode(node) && ts.isPropertyAccessExpression(node.expression)) {
451
+ const calleeText = renderMemberAccessText(node.expression.expression, node.expression.name.text);
452
+ return renderOptionalGuardedAccess(node.expression.expression, `${calleeText}(${argsText})`);
453
+ }
454
+ const calleeText = formatExpressionText(node.expression);
455
+ return `${calleeText}(${argsText})`;
456
+ }
457
+ if (ts.isPrefixUnaryExpression(node)) {
458
+ return `${node.operator === ts.SyntaxKind.PlusPlusToken ? "++" : node.operator === ts.SyntaxKind.MinusMinusToken ? "--" : ts.tokenToString(node.operator) ?? ""}${formatExpressionText(node.operand)}`;
459
+ }
460
+ if (ts.isBinaryExpression(node)) {
461
+ let operator = ts.tokenToString(node.operatorToken.kind) ?? node.operatorToken.getText();
462
+ if (operator === "===") {
463
+ operator = "==";
464
+ }
465
+ else if (operator === "!==") {
466
+ operator = "!=";
467
+ }
468
+ else if (operator === "??") {
469
+ // Nullish coalescing must not use truthiness semantics because 0/false
470
+ // are valid values in TypeScript. Emit a helper call instead.
471
+ const left = formatExpressionText(node.left);
472
+ const right = formatExpressionText(node.right);
473
+ return `cuttlefish_nullish(${left}, ${right})`;
474
+ }
475
+ else if (operator === "**") {
476
+ // C++ has no ** operator — translate to pow()
477
+ const left = formatExpressionText(node.left);
478
+ const right = formatExpressionText(node.right);
479
+ return `pow(${left}, ${right})`;
480
+ }
481
+ return `${formatExpressionText(node.left)} ${operator} ${formatExpressionText(node.right)}`;
482
+ }
483
+ return node.getText();
484
+ };
485
+ if (ts.isNumericLiteral(expr)) {
486
+ const numValue = Number(expr.text);
487
+ // Use original source text to detect float literals — TypeScript normalizes "2.0" to "2" in expr.text
488
+ const originalText = sourceText.substring(expr.pos, expr.end).trim();
489
+ const isFloat = /[.eE]/.test(originalText);
490
+ return { kind: "number", value: numValue, ...(isFloat ? { cppType: "double" } : {}) };
491
+ }
492
+ if (expr.kind === ts.SyntaxKind.RegularExpressionLiteral) {
493
+ const regexText = expr.getText();
494
+ return { kind: "raw", value: `std::regex(${JSON.stringify(regexText)})` };
495
+ }
496
+ if (ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr)) {
497
+ const inner = expressionToIR(expr.expression, sourceText, diagnostics, pointerVars);
498
+ // When the cast involves an `enum class` on one side and an integral type
499
+ // on the other, emit a `static_cast<T>(...)` instead of erasing. C++ enum
500
+ // class has NO implicit conversion, so the erased cast produces invalid
501
+ // C++. The SUPPORT_MATRIX handles this for specific sites (relational,
502
+ // index, Map key, storage boundary) but NOT for the general `as` cast —
503
+ // the user's explicit escape hatch (enum stress test Finding C).
504
+ const targetTypeName = expr.type && ts.isTypeReferenceNode(expr.type) && ts.isIdentifier(expr.type.typeName)
505
+ ? expr.type.typeName.text : "";
506
+ const isTargetEnum = targetTypeName && activeEnumNames.has(targetTypeName) && !activeStringEnumNames.has(targetTypeName);
507
+ if (isTargetEnum) {
508
+ // int → enum: static_cast<EnumType>(value)
509
+ return { kind: "raw", value: `static_cast<${targetTypeName}>(${renderExprAsText(inner)})` };
510
+ }
511
+ // enum → int: detect if the source expression is an enum-typed identifier
512
+ // or enum member access, and the target is an integral C++ type.
513
+ const isIntegralTarget = targetTypeName && /^(int|long|short|char|double|float|uint|int8|int16|int32|int64|size_t)/.test(targetTypeName);
514
+ if (isIntegralTarget) {
515
+ // Check if the inner expression is an enum-typed value. An identifier
516
+ // may be an enum-typed VARIABLE (param, local) — resolve its type from
517
+ // the IR type scope's locals/globals. An enum member access
518
+ // (Mode.Run) resolves via the enum name on the object.
519
+ const innerExpr = expr.expression;
520
+ let sourceEnumName = "";
521
+ if (ts.isIdentifier(innerExpr)) {
522
+ // Direct enum name (e.g. `Mode` used as a value).
523
+ if (activeEnumNames.has(innerExpr.text) && !activeStringEnumNames.has(innerExpr.text)) {
524
+ sourceEnumName = innerExpr.text;
525
+ }
526
+ else {
527
+ // Variable whose type is an enum — resolve from locals/globals.
528
+ const varType = getCurrentIrTypeScope()?.locals.get(innerExpr.text)
529
+ ?? getCurrentIrTypeScope()?.globals.get(innerExpr.text);
530
+ if (varType && activeEnumNames.has(varType) && !activeStringEnumNames.has(varType)) {
531
+ sourceEnumName = varType;
532
+ }
533
+ }
534
+ }
535
+ else if (ts.isPropertyAccessExpression(innerExpr) && ts.isIdentifier(innerExpr.expression)
536
+ && activeEnumNames.has(innerExpr.expression.text) && !activeStringEnumNames.has(innerExpr.expression.text)) {
537
+ sourceEnumName = innerExpr.expression.text;
538
+ }
539
+ if (sourceEnumName) {
540
+ return { kind: "raw", value: `static_cast<${targetTypeName}>(${renderExprAsText(inner)})` };
541
+ }
542
+ }
543
+ return inner;
544
+ }
545
+ if (ts.isAwaitExpression(expr)) {
546
+ return {
547
+ kind: "await",
548
+ value: expressionToIR(expr.expression, sourceText, diagnostics, pointerVars),
549
+ };
550
+ }
551
+ if (expr.kind === ts.SyntaxKind.VoidExpression) {
552
+ const voidExpr = expr;
553
+ const inner = expressionToIR(voidExpr.expression, sourceText, diagnostics, pointerVars);
554
+ const innerText = renderExprAsText(inner);
555
+ return { kind: "raw", value: `(void)(${innerText}), CUTTLEFISH_UNDEFINED` };
556
+ }
557
+ // Handle 'as const' and other type assertions - unwrap and process inner expression
558
+ if (ts.isAsExpression(expr)) {
559
+ return expressionToIR(expr.expression, sourceText, diagnostics, pointerVars);
560
+ }
561
+ // Handle type assertions like (<Type>expr) - unwrap and process inner expression
562
+ if (ts.isTypeAssertionExpression(expr)) {
563
+ return expressionToIR(expr.expression, sourceText, diagnostics, pointerVars);
564
+ }
565
+ // Handle 'satisfies' expressions (TS 4.9+) - unwrap and process inner expression
566
+ if (ts.isSatisfiesExpression?.(expr)) {
567
+ return expressionToIR(expr.expression, sourceText, diagnostics, pointerVars);
568
+ }
569
+ if (expr.kind === ts.SyntaxKind.SatisfiesExpression) {
570
+ return expressionToIR(expr.expression, sourceText, diagnostics, pointerVars);
571
+ }
572
+ // Handle instanceof expressions (must be before generic binary expression handling)
573
+ if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) {
574
+ const object = expressionToIR(expr.left, sourceText, diagnostics);
575
+ const className = expr.right.getText();
576
+ return { kind: "instanceof", object, className };
577
+ }
578
+ // Handle 'in' operator: "key" in obj → map.count() or vector find
579
+ if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.InKeyword) {
580
+ const left = renderExprAsText(expressionToIR(expr.left, sourceText, diagnostics, pointerVars));
581
+ const rightNode = expr.right;
582
+ let rightVarType;
583
+ if (ts.isIdentifier(rightNode)) {
584
+ rightVarType = getCurrentIrTypeScope()?.locals.get(rightNode.text) ?? getCurrentIrTypeScope()?.globals.get(rightNode.text);
585
+ }
586
+ if (rightVarType && parsedIsMap(rightVarType)) {
587
+ return { kind: "raw", value: `(${rightNode.getText()}.count(${left}) > 0)` };
588
+ }
589
+ if (rightVarType && parsedIsVector(rightVarType)) {
590
+ return { kind: "raw", value: `(std::find(${rightNode.getText()}.begin(), ${rightNode.getText()}.end(), ${left}) != ${rightNode.getText()}.end())` };
591
+ }
592
+ if (rightVarType && parsedIsSet(rightVarType)) {
593
+ return { kind: "raw", value: `(${rightNode.getText()}.count(${left}) > 0)` };
594
+ }
595
+ return { kind: "raw", value: `(std::find(${rightNode.getText()}.begin(), ${rightNode.getText()}.end(), ${left}) != ${rightNode.getText()}.end())` };
596
+ }
597
+ if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken) {
598
+ const leftText = renderExprAsText(expressionToIR(expr.left, sourceText, diagnostics, pointerVars));
599
+ const rightText = renderExprAsText(expressionToIR(expr.right, sourceText, diagnostics, pointerVars));
600
+ return { kind: "raw", value: `static_cast<unsigned int>(static_cast<unsigned int>(${leftText}) >> ${rightText})` };
601
+ }
602
+ // Detect string-bearing + chains and fold them into string_concat IR so they
603
+ // flow through the same snprintf / std::string pipeline that template literals use.
604
+ const STRING_RETURNING_METHODS = new Set([
605
+ 'toUpperCase', 'toLowerCase', 'trim', 'replace',
606
+ 'charAt', 'substring', 'slice', 'endsWith', 'includes', 'toString',
607
+ ]);
608
+ const ALL_STRING_METHODS = new Set([
609
+ 'toUpperCase', 'toLowerCase', 'trim', 'replace', 'charAt', 'charCodeAt',
610
+ 'substring', 'slice', 'endsWith', 'startsWith', 'includes', 'indexOf',
611
+ 'lastIndexOf', 'padStart', 'padEnd', 'repeat', 'split', 'toString',
612
+ ]);
613
+ function isStringBearingConcatChain(e) {
614
+ if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e) || ts.isTemplateExpression(e))
615
+ return true;
616
+ if (ts.isIdentifier(e)) {
617
+ if (activeStringVars.has(e.text) || getCurrentIrTypeScope()?.locals.get(e.text) === "std::string")
618
+ return true;
619
+ const varType = getCurrentIrTypeScope()?.locals.get(e.text) ?? getCurrentIrTypeScope()?.globals.get(e.text);
620
+ if (varType === "const char*" || varType === "char*")
621
+ return true;
622
+ // A variable whose declared type is a string enum holds a const char*.
623
+ if (varType && activeStringEnumNames.has(varType))
624
+ return true;
625
+ if (process.env.CF_ENUM_DEBUG && e.text === "c") {
626
+ console.error("[CF_ENUM_DEBUG] c varType=", JSON.stringify(varType), "stringEnums=", JSON.stringify([...activeStringEnumNames]));
627
+ }
628
+ }
629
+ if (ts.isParenthesizedExpression(e)) {
630
+ return isStringBearingConcatChain(e.expression);
631
+ }
632
+ if (ts.isConditionalExpression(e)) {
633
+ return isStringBearingConcatChain(e.whenTrue) || isStringBearingConcatChain(e.whenFalse);
634
+ }
635
+ if (ts.isPropertyAccessExpression(e)) {
636
+ if (e.expression.kind === ts.SyntaxKind.ThisKeyword) {
637
+ const fieldType = getCurrentIrTypeScope()?.locals.get(`this->${e.name.text}`);
638
+ if (fieldType === "std::string" || fieldType === "const char*" || fieldType === "char*")
639
+ return true;
640
+ }
641
+ if (ts.isIdentifier(e.expression)) {
642
+ const objType = getCurrentIrTypeScope()?.locals.get(e.expression.text) ?? getCurrentIrTypeScope()?.globals.get(e.expression.text);
643
+ if (objType) {
644
+ const className = objType.replace(/\*$/, "");
645
+ const classDef = topLevelClasses.get(className);
646
+ if (classDef) {
647
+ const field = classDef.fields.find(f => f.name === e.name.text);
648
+ if (field) {
649
+ const ft = field.cppType;
650
+ if (ft === "std::string" || ft === "const char*" || ft === "char*")
651
+ return true;
652
+ }
653
+ }
654
+ }
655
+ }
656
+ // Handle chained property access: s.player.weaponName where the
657
+ // receiver is itself a property access (e.g. local pointer alias).
658
+ if (ts.isPropertyAccessExpression(e.expression)) {
659
+ const receiverType = resolveExprCppType(e.expression);
660
+ if (receiverType) {
661
+ const className = receiverType.replace(/\*$/, "");
662
+ const classDef = topLevelClasses.get(className);
663
+ if (classDef) {
664
+ const field = classDef.fields.find(f => f.name === e.name.text);
665
+ if (field) {
666
+ const ft = field.cppType;
667
+ if (ft === "std::string" || ft === "const char*" || ft === "char*")
668
+ return true;
669
+ }
670
+ }
671
+ }
672
+ }
673
+ }
674
+ if (ts.isCallExpression(e)) {
675
+ const callType = resolveExprCppType(e);
676
+ if (callType === "std::string" || callType === "const char*" || callType === "char*")
677
+ return true;
678
+ if (ts.isPropertyAccessExpression(e.expression)) {
679
+ const methodName = e.expression.name.text;
680
+ if (STRING_RETURNING_METHODS.has(methodName))
681
+ return true;
682
+ if (ts.isIdentifier(e.expression.expression)) {
683
+ const receiverType = getCurrentIrTypeScope()?.locals.get(e.expression.expression.text);
684
+ if (receiverType === "std::string" || receiverType === "const char*" || activeStringVars.has(e.expression.expression.text))
685
+ return true;
686
+ }
687
+ if (ts.isPropertyAccessExpression(e.expression.expression) && e.expression.expression.expression.kind === ts.SyntaxKind.ThisKeyword) {
688
+ const fieldType = getCurrentIrTypeScope()?.locals.get(`this->${e.expression.expression.name.text}`);
689
+ if (fieldType === "std::string" || fieldType === "const char*")
690
+ return true;
691
+ }
692
+ }
693
+ if (ts.isIdentifier(e.expression)) {
694
+ const fnName = e.expression.text;
695
+ const returnType = getCurrentIrTypeScope()?.locals.get(`fn:${fnName}`) ?? getCurrentIrTypeScope()?.globals.get(`fn:${fnName}`);
696
+ if (returnType === "std::string" || returnType === "const char*")
697
+ return true;
698
+ }
699
+ }
700
+ if (ts.isElementAccessExpression(e) && ts.isIdentifier(e.expression)) {
701
+ const arrType = getCurrentIrTypeScope()?.locals.get(e.expression.text) ?? getCurrentIrTypeScope()?.globals.get(e.expression.text);
702
+ if (arrType) {
703
+ // Element type of a vector, checked for string-ness.
704
+ const elemStr = parsedElementString(arrType);
705
+ if (elemStr && (elemStr === "std::string" || elemStr === "const char*"))
706
+ return true;
707
+ }
708
+ }
709
+ if (ts.isBinaryExpression(e) &&
710
+ e.operatorToken.kind === ts.SyntaxKind.PlusToken) {
711
+ return isStringBearingConcatChain(e.left) || isStringBearingConcatChain(e.right);
712
+ }
713
+ return false;
714
+ }
715
+ function flattenStringConcatParts(e) {
716
+ if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) {
717
+ return e.text ? [{ kind: "string", value: e.text }] : [];
718
+ }
719
+ if (ts.isTemplateExpression(e)) {
720
+ const nested = expressionToIR(e, sourceText, diagnostics, pointerVars);
721
+ return nested.kind === "string_concat" ? nested.parts : [nested];
722
+ }
723
+ if (ts.isBinaryExpression(e) &&
724
+ e.operatorToken.kind === ts.SyntaxKind.PlusToken &&
725
+ isStringBearingConcatChain(e)) {
726
+ return [
727
+ ...flattenStringConcatParts(e.left),
728
+ ...flattenStringConcatParts(e.right),
729
+ ];
730
+ }
731
+ return [{ kind: "template_string", expression: expressionToIR(e, sourceText, diagnostics, pointerVars) }];
732
+ }
733
+ // Route string-bearing + chains into string_concat IR (same path as template literals).
734
+ if (ts.isBinaryExpression(expr) &&
735
+ expr.operatorToken.kind === ts.SyntaxKind.PlusToken &&
736
+ isStringBearingConcatChain(expr)) {
737
+ const parts = flattenStringConcatParts(expr);
738
+ if (parts.length === 1)
739
+ return parts[0];
740
+ return { kind: "string_concat", parts };
741
+ }
742
+ // Nullish coalescing: use a helper instead of truthiness so that
743
+ // values like 0 and false are preserved correctly.
744
+ if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
745
+ const left = renderExprAsText(expressionToIR(expr.left, sourceText, diagnostics, pointerVars));
746
+ const right = renderExprAsText(expressionToIR(expr.right, sourceText, diagnostics, pointerVars));
747
+ return { kind: "raw", value: `cuttlefish_nullish(${left}, ${right})` };
748
+ }
749
+ // Detect type guard pattern: typeof x == "literal" or typeof x != "literal"
750
+ // where x has a known type. Emit a compile-time constant to avoid dead code.
751
+ if (ts.isBinaryExpression(expr) && (expr.operatorToken.kind === ts.SyntaxKind.EqualsToken || expr.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken || expr.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken || expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken || expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken)) {
752
+ const leftIsTypeof = ts.isTypeOfExpression(expr.left);
753
+ const rightIsString = ts.isStringLiteral(expr.right);
754
+ if (leftIsTypeof && rightIsString) {
755
+ const typeofOperand = expr.left.expression;
756
+ if (ts.isIdentifier(typeofOperand)) {
757
+ const varType = getCurrentIrTypeScope()?.locals.get(typeofOperand.text);
758
+ const expectedTypeName = expr.right.text;
759
+ const actualTypeName = varType === "int" || varType === "float" || varType === "double" || varType === "long" || varType === "long long" || varType === "unsigned long long" || varType === "unsigned" || varType === "size_t"
760
+ ? "number"
761
+ : varType === "bool"
762
+ ? "boolean"
763
+ : varType === "std::string"
764
+ ? "string"
765
+ : varType === "void"
766
+ ? "undefined"
767
+ : "object";
768
+ const isEquality = expr.operatorToken.kind === ts.SyntaxKind.EqualsToken || expr.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken || expr.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken;
769
+ const matches = actualTypeName === expectedTypeName;
770
+ return { kind: "boolean", value: isEquality ? matches : !matches };
771
+ }
772
+ }
773
+ }
774
+ // Handle `valueType === null` / `!== null` / `=== undefined` on a value type.
775
+ // `T | null` erases to a value type (vector/map/set/struct), which is never
776
+ // null in C++, so comparing it to nullptr is invalid (`no match for
777
+ // operator==`). Resolve to a compile-time boolean. Demo #9 Finding D.
778
+ //
779
+ // A value type here is any non-pointer C++ type: the STL containers, strings,
780
+ // and — critically — *interface names*, because an `interface Foo` lowers to
781
+ // a C++ `struct Foo` (a value), not a pointer. Classes are always reference
782
+ // types (pointers), so they are excluded by the `!parsedIsPointer(vt)` filter
783
+ // (and by not being in topLevelInterfaceNames). Demo #18 Finding A: a struct
784
+ // returned from a function/method and stored in a local (`let s: Account |
785
+ // null = find(); s === null`) is now recognised as a value type and lowers to
786
+ // a compile-time `false` instead of the invalid `s == CUTTLEFISH_UNDEFINED`.
787
+ if (ts.isBinaryExpression(expr)
788
+ && (expr.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken
789
+ || expr.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken
790
+ || expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken
791
+ || expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken)) {
792
+ const isNullRhs = expr.right.kind === ts.SyntaxKind.NullKeyword || expr.right.kind === ts.SyntaxKind.UndefinedKeyword;
793
+ const isNullLhs = expr.left.kind === ts.SyntaxKind.NullKeyword || expr.left.kind === ts.SyntaxKind.UndefinedKeyword;
794
+ if (isNullRhs || isNullLhs) {
795
+ const valueNode = isNullRhs ? expr.left : expr.right;
796
+ // Resolve the operand's C++ type. Identifiers read it from the active
797
+ // local/global type maps; an inline call (`findAccount(1) === null` or
798
+ // `this.find(1) === null`) is resolved from the callee's declared return
799
+ // type — see resolveCallReturnTypeForNullGuard below.
800
+ let vt;
801
+ if (ts.isIdentifier(valueNode)) {
802
+ vt = getCurrentIrTypeScope()?.locals.get(valueNode.text) ?? getCurrentIrTypeScope()?.globals.get(valueNode.text);
803
+ }
804
+ else if (ts.isCallExpression(valueNode)) {
805
+ vt = resolveCallReturnTypeForNullGuard(valueNode, sourceText);
806
+ }
807
+ else if (ts.isParenthesizedExpression(valueNode) || ts.isNonNullExpression(valueNode)) {
808
+ const inner = ts.isParenthesizedExpression(valueNode) ? valueNode.expression : valueNode.expression;
809
+ if (ts.isIdentifier(inner)) {
810
+ vt = getCurrentIrTypeScope()?.locals.get(inner.text) ?? getCurrentIrTypeScope()?.globals.get(inner.text);
811
+ }
812
+ else if (ts.isCallExpression(inner)) {
813
+ vt = resolveCallReturnTypeForNullGuard(inner, sourceText);
814
+ }
815
+ }
816
+ // A "value type" for null-comparison purposes: a container, std::string,
817
+ // or a value-typed interface/class name. Pointers are excluded (they
818
+ // compare against nullptr, not {}).
819
+ const isValueType = vt && !parsedIsPointer(vt) && (parsedIsVector(vt)
820
+ || parsedIsMap(vt)
821
+ || parsedIsSet(vt)
822
+ || parsedIsStdString(vt)
823
+ || topLevelInterfaceNames.has(parsedBareString(vt))
824
+ || topLevelClassNames.has(parsedBareString(vt)));
825
+ if (isValueType) {
826
+ const isEquality = expr.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken || expr.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken;
827
+ // A value type is never null → `=== null` is false, `!== null` is true.
828
+ return { kind: "boolean", value: isEquality ? false : true };
829
+ }
830
+ }
831
+ }
832
+ // Standalone typeof x emits a type-name string literal.
833
+ if (ts.isTypeOfExpression(expr)) {
834
+ const operand = expr.expression;
835
+ if (ts.isIdentifier(operand)) {
836
+ const varType = getCurrentIrTypeScope()?.locals.get(operand.text) ?? getCurrentIrTypeScope()?.globals.get(operand.text);
837
+ // The cuttlefish intNN_t/uintNN_t family maps to TS `number`.
838
+ const isNumberType = varType === "int" || varType === "float" || varType === "double"
839
+ || varType === "long" || varType === "long long" || varType === "unsigned long long"
840
+ || varType === "unsigned" || varType === "size_t"
841
+ || (varType !== undefined && /^(int|uint)(8|16|32|64)_t$/.test(varType));
842
+ const typeName = isNumberType
843
+ ? "number"
844
+ : varType === "bool"
845
+ ? "boolean"
846
+ : varType === "std::string"
847
+ ? "string"
848
+ : varType === "void"
849
+ ? "undefined"
850
+ : "object";
851
+ return { kind: "string", value: typeName };
852
+ }
853
+ if (ts.isNumericLiteral(operand))
854
+ return { kind: "string", value: "number" };
855
+ if (ts.isStringLiteral(operand) || ts.isNoSubstitutionTemplateLiteral(operand))
856
+ return { kind: "string", value: "string" };
857
+ if (operand.kind === ts.SyntaxKind.TrueKeyword || operand.kind === ts.SyntaxKind.FalseKeyword)
858
+ return { kind: "string", value: "boolean" };
859
+ if (ts.isArrayLiteralExpression(operand))
860
+ return { kind: "string", value: "object" };
861
+ if (ts.isObjectLiteralExpression(operand))
862
+ return { kind: "string", value: "object" };
863
+ if (ts.isFunctionExpression(operand) || ts.isArrowFunction(operand))
864
+ return { kind: "string", value: "function" };
865
+ if (ts.isNewExpression(operand))
866
+ return { kind: "string", value: "object" };
867
+ if (ts.isPropertyAccessExpression(operand)) {
868
+ if (operand.name.text === "length")
869
+ return { kind: "string", value: "number" };
870
+ return { kind: "string", value: "object" };
871
+ }
872
+ if (ts.isCallExpression(operand) && ts.isPropertyAccessExpression(operand.expression)) {
873
+ const methodName = operand.expression.name.text;
874
+ if (["push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "concat", "slice", "filter", "map", "reduce", "find", "findIndex", "every", "some", "forEach", "includes", "indexOf", "lastIndexOf", "join"].includes(methodName)) {
875
+ return { kind: "string", value: "object" };
876
+ }
877
+ }
878
+ return { kind: "string", value: "object" };
879
+ }
880
+ // Recurse into binary expressions so nested TypeCAD calls are translated correctly.
881
+ if (ts.isBinaryExpression(expr)) {
882
+ let operator = ts.tokenToString(expr.operatorToken.kind) ?? expr.operatorToken.getText();
883
+ if (operator === "===")
884
+ operator = "==";
885
+ else if (operator === "!==")
886
+ operator = "!=";
887
+ return {
888
+ kind: "binary",
889
+ left: expressionToIR(expr.left, sourceText, diagnostics, pointerVars),
890
+ operator,
891
+ right: expressionToIR(expr.right, sourceText, diagnostics, pointerVars),
892
+ };
893
+ }
894
+ // Preserve parenthesized expressions as a `paren` IR node so that explicit
895
+ // grouping from the TS source is retained in the emitted C++ (e.g. `(2+3)*4`).
896
+ if (ts.isParenthesizedExpression(expr)) {
897
+ return { kind: "paren", inner: expressionToIR(expr.expression, sourceText, diagnostics, pointerVars) };
898
+ }
899
+ // Recurse into prefix unary so nested calls are translated correctly.
900
+ if (ts.isPrefixUnaryExpression(expr)) {
901
+ const operator = ts.tokenToString(expr.operator) ?? "";
902
+ return {
903
+ kind: "unary",
904
+ operator,
905
+ operand: expressionToIR(expr.operand, sourceText, diagnostics, pointerVars),
906
+ };
907
+ }
908
+ // Handle postfix unary (i++, i--) so they compose correctly in IR.
909
+ if (ts.isPostfixUnaryExpression(expr)) {
910
+ const operator = ts.tokenToString(expr.operator) ?? "";
911
+ return {
912
+ kind: "unary",
913
+ operator,
914
+ operand: expressionToIR(expr.operand, sourceText, diagnostics, pointerVars),
915
+ postfix: true,
916
+ };
917
+ }
918
+ if (ts.isCallExpression(expr)) {
919
+ // ---- board() / boardResolve() — compile-time board constant lookup ----
920
+ if (ts.isIdentifier(expr.expression) && (expr.expression.text === "board" || expr.expression.text === "boardResolve")) {
921
+ const pathArg = expr.arguments[0];
922
+ if (pathArg && ts.isStringLiteral(pathArg)) {
923
+ const bc = getCurrentBoardConstants();
924
+ if (bc) {
925
+ const val = bc.get(pathArg.text);
926
+ if (val !== undefined) {
927
+ // Preserve the value's type: string board constants (e.g.
928
+ // "architecture" → "avr") must render as C++ string literals, not
929
+ // be coerced via Number() which yields NaN.
930
+ if (typeof val === "string")
931
+ return { kind: "string", value: val };
932
+ return { kind: "number", value: Number(val) };
933
+ }
934
+ }
935
+ }
936
+ // Unresolved — emit 0 so the value is at least syntactically valid
937
+ return { kind: "number", value: 0 };
938
+ }
939
+ // ---- HAL inline evaluator for expression context ----
940
+ const halResult = tryResolveHALExpression(expr, sourceText, diagnostics, pointerVars);
941
+ if (halResult)
942
+ return halResult.ir;
943
+ // ---- UI signal read: temp() → temp (the device variable) ----
944
+ // A signal lowers to a plain variable, so calling it (the Signal<T>()
945
+ // accessor) is just a read of that variable.
946
+ if (ts.isIdentifier(expr.expression) &&
947
+ expr.arguments.length === 0 &&
948
+ isSignalName(expr.expression.text)) {
949
+ return { kind: "identifier", value: expr.expression.text };
950
+ }
951
+ // ---- UI element .value read: screen.led.value → __ui_nodes[N].value ----
952
+ if (ts.isPropertyAccessExpression(expr.expression) &&
953
+ expr.expression.name.text === "value" &&
954
+ ts.isPropertyAccessExpression(expr.expression.expression) &&
955
+ ts.isIdentifier(expr.expression.expression.expression)) {
956
+ const treeName = expr.expression.expression.expression.text;
957
+ const elemId = expr.expression.expression.name.text;
958
+ const nodeIdx = resolveElementValue(treeName, elemId);
959
+ if (nodeIdx !== undefined) {
960
+ return { kind: "raw", value: `__ui_nodes[${nodeIdx}].value` };
961
+ }
962
+ }
963
+ // ---- UI element .text read: screen.ssid.text → __ui_nodes[N].textBuffer ----
964
+ if (ts.isPropertyAccessExpression(expr.expression) &&
965
+ expr.expression.name.text === "text" &&
966
+ ts.isPropertyAccessExpression(expr.expression.expression) &&
967
+ ts.isIdentifier(expr.expression.expression.expression)) {
968
+ const treeName = expr.expression.expression.expression.text;
969
+ const elemId = expr.expression.expression.name.text;
970
+ const nodeIdx = resolveElementValue(treeName, elemId);
971
+ if (nodeIdx !== undefined) {
972
+ return { kind: "raw", value: `__ui_nodes[${nodeIdx}].textBuffer` };
973
+ }
974
+ }
975
+ // Warn about optional chaining on call expressions — we preserve a null guard,
976
+ // but the runtime semantics are still only approximate compared to TypeScript.
977
+ if (isOptionalChainNode(expr)) {
978
+ diagnostics.push(makeDiagnostic(sourceText, expr.pos, "Optional chaining (?.) is lowered with an approximate null guard in C++; semantics may differ from TypeScript.", "warning", "TS2CPP_OPTIONAL_CHAINING"));
979
+ if (ts.isPropertyAccessExpression(expr.expression)) {
980
+ const argsText = expr.arguments
981
+ .map(arg => renderExprAsText(expressionToIR(arg, sourceText, diagnostics, pointerVars)))
982
+ .join(", ");
983
+ const calleeText = renderMemberAccessText(expr.expression.expression, expr.expression.name.text);
984
+ return {
985
+ kind: "raw",
986
+ value: renderOptionalGuardedAccess(expr.expression.expression, `${calleeText}(${argsText})`),
987
+ };
988
+ }
989
+ // Bare-identifier optional call: `fn?.()`. Without this branch the call
990
+ // falls through to the generic path and emits `fn()` unconditionally —
991
+ // calling an empty std::function throws std::bad_function_call. Wrap the
992
+ // call in the same null guard as the property-access case.
993
+ if (ts.isIdentifier(expr.expression)) {
994
+ const argsText = expr.arguments
995
+ .map(arg => renderExprAsText(expressionToIR(arg, sourceText, diagnostics, pointerVars)))
996
+ .join(", ");
997
+ const calleeText = expr.expression.text;
998
+ return {
999
+ kind: "raw",
1000
+ value: renderOptionalGuardedAccess(expr.expression, `${calleeText}(${argsText})`),
1001
+ };
1002
+ }
1003
+ }
1004
+ // ---- Pin factory constant-folding ---------------------------------------
1005
+ // createDigitalPin(pin, gpio), createPWMPin(pin, gpio), etc. are folded to
1006
+ // just the pin number (first argument) at compile time.
1007
+ if (ts.isIdentifier(expr.expression) && PIN_FACTORY_FUNCTIONS.has(expr.expression.text)) {
1008
+ if (expr.arguments.length >= 1 && ts.isNumericLiteral(expr.arguments[0])) {
1009
+ return { kind: "number", value: Number(expr.arguments[0].text) };
1010
+ }
1011
+ }
1012
+ // ---- Helper function constant-folding -----------------------------------
1013
+ // pinNumber(n) is folded to just the number value at compile time.
1014
+ if (ts.isIdentifier(expr.expression) && CONSTANT_FOLD_FUNCTIONS.has(expr.expression.text)) {
1015
+ if (expr.arguments.length >= 1 && ts.isNumericLiteral(expr.arguments[0])) {
1016
+ return { kind: "number", value: Number(expr.arguments[0].text) };
1017
+ }
1018
+ // Also handle nested expressions like pinNumber(someVar)
1019
+ if (expr.arguments.length >= 1) {
1020
+ return expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars);
1021
+ }
1022
+ }
1023
+ const arrayMethodRes = tryLowerArrayAndStringMethods(expr, sourceText, diagnostics, pointerVars);
1024
+ if (arrayMethodRes !== null) {
1025
+ return arrayMethodRes;
1026
+ }
1027
+ if (ts.isPropertyAccessExpression(expr.expression) && ALL_STRING_METHODS.has(expr.expression.name.text)) {
1028
+ const receiver = expr.expression.expression;
1029
+ const isStringLiteralReceiver = ts.isStringLiteral(receiver) || ts.isNoSubstitutionTemplateLiteral(receiver);
1030
+ const isTemplateExprReceiver = ts.isTemplateExpression(receiver);
1031
+ const isStringVarReceiver = ts.isIdentifier(receiver) && (activeStringVars.has(receiver.text) || getCurrentIrTypeScope()?.locals.get(receiver.text) === "std::string");
1032
+ if (isStringLiteralReceiver || isTemplateExprReceiver || isStringVarReceiver) {
1033
+ const receiverIR = expressionToIR(receiver, sourceText, diagnostics, pointerVars);
1034
+ let receiverText = renderExprAsText(receiverIR);
1035
+ if (receiver.kind === ts.SyntaxKind.StringLiteral || receiver.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
1036
+ receiverText = `std::string(${receiverText})`;
1037
+ }
1038
+ const methodName = expr.expression.name.text;
1039
+ const argsText = expr.arguments.map(a => renderExprAsText(expressionToIR(a, sourceText, diagnostics, pointerVars))).join(", ");
1040
+ return { kind: "raw", value: `${receiverText}.${methodName}(${argsText})` };
1041
+ }
1042
+ }
1043
+ if (ts.isIdentifier(expr.expression) && expr.expression.text === "Error") {
1044
+ const message = expr.arguments.length > 0
1045
+ ? renderExprAsText(expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars))
1046
+ : '"error"';
1047
+ return { kind: "raw", value: `std::runtime_error(${message})` };
1048
+ }
1049
+ if (ts.isIdentifier(expr.expression)) {
1050
+ const fnName = expr.expression.text;
1051
+ if (fnName === "parseInt" && expr.arguments.length >= 1) {
1052
+ const argText = renderExprAsText(expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars));
1053
+ // atoi/atof take const char*; the arg is typically a std::string.
1054
+ // Use .c_str() so the conversion compiles.
1055
+ return { kind: "raw", value: `atoi((${argText}).c_str())` };
1056
+ }
1057
+ if (fnName === "parseFloat" && expr.arguments.length >= 1) {
1058
+ const argText = renderExprAsText(expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars));
1059
+ return { kind: "raw", value: `atof((${argText}).c_str())` };
1060
+ }
1061
+ if (fnName === "isNaN" && expr.arguments.length >= 1) {
1062
+ const argText = renderExprAsText(expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars));
1063
+ return { kind: "raw", value: `std::isnan(${argText})` };
1064
+ }
1065
+ if (fnName === "isFinite" && expr.arguments.length >= 1) {
1066
+ const argText = renderExprAsText(expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars));
1067
+ return { kind: "raw", value: `std::isfinite(${argText})` };
1068
+ }
1069
+ if (fnName === "Number" && expr.arguments.length >= 1) {
1070
+ return expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars);
1071
+ }
1072
+ if (fnName === "String" && expr.arguments.length >= 1) {
1073
+ const argIR = expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars);
1074
+ return { kind: "template_string", expression: argIR };
1075
+ }
1076
+ if (fnName === "Boolean" && expr.arguments.length >= 1) {
1077
+ const argIR = expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars);
1078
+ return { kind: "raw", value: `static_cast<bool>(${renderExprAsText(argIR)})` };
1079
+ }
1080
+ }
1081
+ // Handle static method calls on built-in objects: Array.isArray, Object.keys/values/entries/assign
1082
+ if (ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression)) {
1083
+ const objName = expr.expression.expression.text;
1084
+ const methodName = expr.expression.name.text;
1085
+ if (objName === "Array" && methodName === "isArray" && expr.arguments.length === 1) {
1086
+ const argIR = expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars);
1087
+ const argText = renderExprAsText(argIR);
1088
+ let argType;
1089
+ if (ts.isIdentifier(expr.arguments[0])) {
1090
+ argType = getCurrentIrTypeScope()?.locals.get(expr.arguments[0].text) ?? getCurrentIrTypeScope()?.globals.get(expr.arguments[0].text);
1091
+ }
1092
+ if (argType && parsedIsVector(argType)) {
1093
+ return { kind: "boolean", value: true };
1094
+ }
1095
+ return { kind: "boolean", value: false };
1096
+ }
1097
+ if (objName === "Object" && expr.arguments.length >= 1) {
1098
+ const argNode = expr.arguments[0];
1099
+ const argIR = expressionToIR(argNode, sourceText, diagnostics, pointerVars);
1100
+ const argText = renderExprAsText(argIR);
1101
+ let argType;
1102
+ if (ts.isIdentifier(argNode)) {
1103
+ argType = getCurrentIrTypeScope()?.locals.get(argNode.text) ?? getCurrentIrTypeScope()?.globals.get(argNode.text);
1104
+ }
1105
+ else if (ts.isPropertyAccessExpression(argNode) && argNode.expression.kind === ts.SyntaxKind.ThisKeyword) {
1106
+ // `Object.keys(this.field)` — resolve the field's type via the
1107
+ // this->field map populated during class IR build.
1108
+ argType = getCurrentIrTypeScope()?.locals.get(`this->${argNode.name.text}`);
1109
+ if (!argType) {
1110
+ argType = getCurrentIrTypeScope()?.classFields.get(`this->${argNode.name.text}`);
1111
+ }
1112
+ }
1113
+ if (methodName === "keys") {
1114
+ if (argType && parsedIsMap(argType)) {
1115
+ return { kind: "raw", value: `__tc_mapKeys(${argText})` };
1116
+ }
1117
+ if (ts.isObjectLiteralExpression(argNode)) {
1118
+ const fieldNames = argNode.properties
1119
+ .filter(ts.isPropertyAssignment)
1120
+ .filter(p => ts.isIdentifier(p.name))
1121
+ .map(p => p.name.text);
1122
+ return { kind: "array", elementType: "const char*", elements: fieldNames.map(n => ({ kind: "string", value: n })) };
1123
+ }
1124
+ return emitUnsupportedExpression("Object.keys on non-map types is unsupported.");
1125
+ }
1126
+ if (methodName === "values") {
1127
+ if (argType && parsedIsMap(argType)) {
1128
+ return { kind: "raw", value: `__tc_mapValues(${argText})` };
1129
+ }
1130
+ if (ts.isObjectLiteralExpression(argNode)) {
1131
+ const elements = argNode.properties
1132
+ .filter(ts.isPropertyAssignment)
1133
+ .map(p => expressionToIR(p.initializer, sourceText, diagnostics, pointerVars));
1134
+ return { kind: "array", elementType: "auto", elements };
1135
+ }
1136
+ return emitUnsupportedExpression("Object.values on non-map types is unsupported.");
1137
+ }
1138
+ if (methodName === "entries") {
1139
+ if (argType && parsedIsMap(argType)) {
1140
+ return { kind: "raw", value: `__tc_mapEntries(${argText})` };
1141
+ }
1142
+ return emitUnsupportedExpression("Object.entries on non-map types is unsupported.");
1143
+ }
1144
+ if (methodName === "assign" && expr.arguments.length === 2) {
1145
+ const targetText = argText;
1146
+ const srcIR = expressionToIR(expr.arguments[1], sourceText, diagnostics, pointerVars);
1147
+ const srcText = renderExprAsText(srcIR);
1148
+ return { kind: "raw", value: `(${targetText} = ${srcText})` };
1149
+ }
1150
+ if (methodName === "fromEntries" && expr.arguments.length === 1) {
1151
+ const entriesNode = expr.arguments[0];
1152
+ const entriesIR = expressionToIR(entriesNode, sourceText, diagnostics, pointerVars);
1153
+ const entriesText = renderExprAsText(entriesIR);
1154
+ return { kind: "raw", value: `__tc_fromEntries(${entriesText})` };
1155
+ }
1156
+ }
1157
+ if (objName === "JSON") {
1158
+ if (methodName === "stringify" && expr.arguments.length >= 1) {
1159
+ const argIR = expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars);
1160
+ const argText = renderExprAsText(argIR);
1161
+ return { kind: "raw", value: `__tc_jsonStringify(${argText})` };
1162
+ }
1163
+ if (methodName === "parse" && expr.arguments.length >= 1) {
1164
+ const argIR = expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars);
1165
+ const argText = renderExprAsText(argIR);
1166
+ return { kind: "raw", value: `__tc_jsonParse(${argText})` };
1167
+ }
1168
+ }
1169
+ }
1170
+ if (ts.isIdentifier(expr.expression) && expr.expression.text === "defineBoardManifest" && expr.arguments.length === 1) {
1171
+ return expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars);
1172
+ }
1173
+ // Handle method calls like this.method() or obj.method()
1174
+ // Use -> for pointer variables (from 'new') and for 'this', . for value types
1175
+ let calleeText;
1176
+ if (ts.isPropertyAccessExpression(expr.expression)) {
1177
+ const receiver = expr.expression.expression;
1178
+ const rawMethodName = expr.expression.name.kind === ts.SyntaxKind.PrivateIdentifier
1179
+ ? `__priv_${expr.expression.name.text.substring(1)}`
1180
+ : expr.expression.name.text;
1181
+ // NOTE: do NOT escapeCppKeyword before the Map/Set method checks below —
1182
+ // `m.delete(k)` would become `methodName === "delete_"` and miss the
1183
+ // map→erase lowering (demo #8 Finding D). Escape only when building the
1184
+ // final callee text for the generic path.
1185
+ const methodName = rawMethodName;
1186
+ // --- .toFixed(digits) on numeric values ---
1187
+ if (methodName === "toFixed" && expr.arguments.length >= 1) {
1188
+ const receiverIR = expressionToIR(receiver, sourceText, diagnostics, pointerVars);
1189
+ const digitsArg = expr.arguments[0];
1190
+ let digits;
1191
+ if (ts.isNumericLiteral(digitsArg)) {
1192
+ digits = parseInt(digitsArg.text, 10);
1193
+ }
1194
+ else {
1195
+ const digitsIR = expressionToIR(digitsArg, sourceText, diagnostics, pointerVars);
1196
+ if (digitsIR.kind === "number" && Number.isInteger(digitsIR.value)) {
1197
+ digits = digitsIR.value;
1198
+ }
1199
+ else {
1200
+ digits = 0;
1201
+ }
1202
+ }
1203
+ const receiverText = renderExprAsText(receiverIR);
1204
+ return { kind: "raw", value: `__tc_toFixed(${receiverText}, ${digits})` };
1205
+ }
1206
+ // --- Map/Set iteration methods: .values() / .keys() / .entries() ---
1207
+ // These lower to the same __tc_mapValues/__tc_mapKeys/__tc_mapEntries
1208
+ // helpers used for Object.values(map) etc. (defined in the native
1209
+ // strategy). They return std::vector<V/K/pair>, so a downstream
1210
+ // for...of iterates the values/keys/pairs correctly — NOT the raw
1211
+ // std::pair entries of the underlying std::map (demo #15 fix A).
1212
+ if ((methodName === "values" || methodName === "keys" || methodName === "entries") && expr.arguments.length === 0) {
1213
+ let receiverType;
1214
+ if (ts.isIdentifier(receiver)) {
1215
+ receiverType = getCurrentIrTypeScope()?.locals.get(receiver.text) ?? getCurrentIrTypeScope()?.globals.get(receiver.text);
1216
+ }
1217
+ else if (ts.isPropertyAccessExpression(receiver) && receiver.expression.kind === ts.SyntaxKind.ThisKeyword) {
1218
+ receiverType = getCurrentIrTypeScope()?.locals.get(`this->${receiver.name.text}`);
1219
+ }
1220
+ if (receiverType && (parsedIsMap(receiverType) || parsedIsSet(receiverType))) {
1221
+ const recIR = expressionToIR(receiver, sourceText, diagnostics, pointerVars);
1222
+ const recText = renderExprAsText(recIR);
1223
+ // Set.values()/keys() both yield the elements; entries() yields
1224
+ // pair<elem,elem>. Map.values()/keys()/entries() are as expected.
1225
+ const isSet = parsedIsSet(receiverType);
1226
+ const helper = methodName === "values"
1227
+ ? (isSet ? "__tc_setValues" : "__tc_mapValues")
1228
+ : methodName === "keys"
1229
+ ? (isSet ? "__tc_setValues" : "__tc_mapKeys")
1230
+ : (isSet ? "__tc_setEntries" : "__tc_mapEntries");
1231
+ return { kind: "raw", value: `${helper}(${recText})` };
1232
+ }
1233
+ }
1234
+ // --- Map/Set method lowering ---
1235
+ if ((methodName === "set" || methodName === "get" || methodName === "has" || methodName === "delete" || methodName === "add") && expr.arguments.length >= 1) {
1236
+ let receiverType;
1237
+ let receiverText;
1238
+ if (ts.isIdentifier(receiver)) {
1239
+ receiverType = getCurrentIrTypeScope()?.locals.get(receiver.text) ?? getCurrentIrTypeScope()?.globals.get(receiver.text);
1240
+ }
1241
+ else if (ts.isPropertyAccessExpression(receiver) && receiver.expression.kind === ts.SyntaxKind.ThisKeyword) {
1242
+ receiverType = getCurrentIrTypeScope()?.locals.get(`this->${receiver.name.text}`);
1243
+ }
1244
+ if (receiverType && (parsedIsMap(receiverType) || parsedIsSet(receiverType))) {
1245
+ const recIR = expressionToIR(receiver, sourceText, diagnostics, pointerVars);
1246
+ receiverText = renderExprAsText(recIR);
1247
+ const arg0IR = expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars);
1248
+ let arg0Text = renderExprAsText(arg0IR);
1249
+ // When the key is an enum-typed operand and the container's key type
1250
+ // is integral, wrap it in static_cast so it matches the comparator /
1251
+ // converts the enum class to the integral key. Handles BOTH shapes:
1252
+ // - an enum member access (Color.Red) — detected via activeEnumNames; and
1253
+ // - a bare identifier whose declared type is an enum (k where k: K)
1254
+ // — resolved through the in-scope IR type map. Demo #28 Finding E
1255
+ // review: previously only the enum-member shape was cast, so a
1256
+ // bare enum-typed key variable on .has/.get/.delete (expression
1257
+ // form) compiled to an uncast map.count(k)/map.at(k) and failed.
1258
+ const receiverIr = parseCppType(receiverType);
1259
+ const mapKeyType = receiverIr.kind === "map" ? renderCppType(receiverIr.key) : "";
1260
+ const keyIsIntegral = /^(int|int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|size_t|long|short|unsigned|char)$/.test(mapKeyType);
1261
+ let keyIsEnum = false;
1262
+ if (keyIsIntegral) {
1263
+ const keyArg = expr.arguments[0];
1264
+ if (ts.isPropertyAccessExpression(keyArg) && ts.isIdentifier(keyArg.expression)) {
1265
+ keyIsEnum = activeEnumNames.has(keyArg.expression.text);
1266
+ }
1267
+ else if (ts.isIdentifier(keyArg)) {
1268
+ const keyVarType = getCurrentIrTypeScope()?.locals.get(keyArg.text) ?? getCurrentIrTypeScope()?.globals.get(keyArg.text);
1269
+ if (keyVarType && activeEnumNames.has(keyVarType)) {
1270
+ keyIsEnum = true;
1271
+ }
1272
+ }
1273
+ }
1274
+ if (keyIsEnum) {
1275
+ arg0Text = `static_cast<${mapKeyType}>(${arg0Text})`;
1276
+ }
1277
+ if (parsedIsMap(receiverType)) {
1278
+ if (methodName === "set" && expr.arguments.length >= 2) {
1279
+ const arg1IR = expressionToIR(expr.arguments[1], sourceText, diagnostics, pointerVars);
1280
+ const arg1Text = renderExprAsText(arg1IR);
1281
+ return { kind: "raw", value: `(${receiverText}[${arg0Text}] = ${arg1Text})` };
1282
+ }
1283
+ if (methodName === "get") {
1284
+ // Use .at() (const-correct, throws on miss) rather than operator[]
1285
+ // (non-const, fails on a const-bound Map, silently inserts on miss).
1286
+ // The idiomatic m.get(k)! asserts presence, matching .at() contract. (demo #15 fix B)
1287
+ return { kind: "raw", value: `${receiverText}.at(${arg0Text})` };
1288
+ }
1289
+ if (methodName === "has") {
1290
+ return { kind: "raw", value: `(${receiverText}.count(${arg0Text}) > 0)` };
1291
+ }
1292
+ if (methodName === "delete") {
1293
+ return { kind: "raw", value: `(${receiverText}.erase(${arg0Text}) > 0)` };
1294
+ }
1295
+ }
1296
+ if (parsedIsSet(receiverType)) {
1297
+ if (methodName === "add") {
1298
+ return { kind: "raw", value: `${receiverText}.insert(${arg0Text})` };
1299
+ }
1300
+ if (methodName === "has") {
1301
+ return { kind: "raw", value: `(${receiverText}.count(${arg0Text}) > 0)` };
1302
+ }
1303
+ if (methodName === "delete") {
1304
+ return { kind: "raw", value: `(${receiverText}.erase(${arg0Text}) > 0)` };
1305
+ }
1306
+ }
1307
+ }
1308
+ }
1309
+ // Check if it's a this.method() call - in C++, this is a pointer so use ->
1310
+ if (receiver.kind === ts.SyntaxKind.ThisKeyword) {
1311
+ calleeText = `this->${escapeCppKeyword(methodName)}`;
1312
+ }
1313
+ else if (ts.isIdentifier(receiver) && receiver.text === "Math") {
1314
+ const mathMethod = expr.expression.name.text;
1315
+ if (mathMethod === "random") {
1316
+ calleeText = "__tc_random";
1317
+ }
1318
+ else if ((mathMethod === "max" || mathMethod === "min") && expr.arguments && expr.arguments.length >= 1) {
1319
+ const op = mathMethod === "max" ? ">" : "<";
1320
+ const args = expr.arguments.map(a => expressionToIR(a, sourceText, diagnostics, pointerVars));
1321
+ let result = args[0];
1322
+ for (let i = 1; i < args.length; i++) {
1323
+ result = {
1324
+ kind: "ternary",
1325
+ condition: { kind: "binary", left: result, operator: op, right: args[i] },
1326
+ whenTrue: result,
1327
+ whenFalse: args[i],
1328
+ };
1329
+ }
1330
+ return result;
1331
+ }
1332
+ else {
1333
+ calleeText = `std::${mathMethod}`;
1334
+ }
1335
+ }
1336
+ else if (ts.isIdentifier(receiver) && (hoistedNestedClasses.some(c => c.name === receiver.text) || nestedClassAliases.has(receiver.text) || topLevelClassNames.has(receiver.text))) {
1337
+ // Static method call on a hoisted or top-level class: use :: with resolved name
1338
+ const resolvedName = nestedClassAliases.get(receiver.text) ?? receiver.text;
1339
+ calleeText = `${resolvedName}::${methodName}`;
1340
+ }
1341
+ else if (ts.isIdentifier(receiver) && activeNamespaceNames.has(receiver.text)) {
1342
+ // Namespace method call: use ::
1343
+ calleeText = `${receiver.text}::${methodName}`;
1344
+ }
1345
+ else {
1346
+ const objText = renderExprAsText(expressionToIR(receiver, sourceText, diagnostics, pointerVars));
1347
+ let accessor = ".";
1348
+ if (ts.isIdentifier(receiver) && (pointerVars.has(receiver.text) || parsedIsPointer(getCurrentIrTypeScope()?.locals.get(receiver.text) ?? "") || parsedIsPointer(getCurrentIrTypeScope()?.globals.get(receiver.text) ?? ""))) {
1349
+ accessor = "->";
1350
+ }
1351
+ else if (ts.isPropertyAccessExpression(receiver)
1352
+ && receiver.expression.kind === ts.SyntaxKind.ThisKeyword
1353
+ && ts.isIdentifier(receiver.name)) {
1354
+ const fieldType = getCurrentIrTypeScope()?.locals.get(`this->${receiver.name.text}`);
1355
+ if (fieldType && parsedIsPointer(fieldType)) {
1356
+ accessor = "->";
1357
+ }
1358
+ }
1359
+ else if (ts.isCallExpression(receiver) && ts.isPropertyAccessExpression(receiver.expression)) {
1360
+ const innerReceiver = receiver.expression.expression;
1361
+ const innerMethodName = receiver.expression.name.text;
1362
+ if (ts.isIdentifier(innerReceiver) && pointerVars.has(innerReceiver.text)) {
1363
+ // Check if the inner method returns a pointer type (for method chaining)
1364
+ let className = pointerVars.get(innerReceiver.text);
1365
+ // Resolve nested class aliases (e.g., "Builder" → "__tc_fn6__Builder")
1366
+ if (className)
1367
+ className = nestedClassAliases.get(className) ?? className;
1368
+ const cls = className ? hoistedNestedClasses.find(c => c.name === className) : undefined;
1369
+ const chainMethod = cls?.methods.find(m => m.name === innerMethodName);
1370
+ if (chainMethod && parsedIsPointer(chainMethod.returnType)) {
1371
+ accessor = "->";
1372
+ }
1373
+ }
1374
+ else if (ts.isIdentifier(innerReceiver) && topLevelClassNames.has(innerReceiver.text)) {
1375
+ // Static method call on a top-level class returning a pointer
1376
+ const cls = topLevelClasses.get(innerReceiver.text);
1377
+ if (!cls) {
1378
+ // Cross-module class: factory methods typically return class instances (pointers)
1379
+ accessor = "->";
1380
+ }
1381
+ else {
1382
+ const chainMethod = cls.methods.find(m => m.name === innerMethodName);
1383
+ if (chainMethod && (parsedIsPointer(chainMethod.returnType) || chainMethod.returnType === innerReceiver.text)) {
1384
+ accessor = "->";
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+ if (accessor === ".") {
1390
+ const receiverType = resolveExprCppType(receiver);
1391
+ if (receiverType && parsedIsPointer(receiverType)) {
1392
+ accessor = "->";
1393
+ }
1394
+ }
1395
+ calleeText = `${objText}${accessor}${escapeCppKeyword(methodName)}`;
1396
+ }
1397
+ }
1398
+ else {
1399
+ const rawText = expr.expression.getText();
1400
+ calleeText = nestedFunctionAliases.get(rawText) ?? rawText;
1401
+ }
1402
+ const argIRs = [];
1403
+ for (const arg of expr.arguments) {
1404
+ if (ts.isSpreadElement(arg)) {
1405
+ // Spreading into a call. Cuttlefish lowers every rest parameter
1406
+ // (`...args: T[]`) to a SINGLE `const std::vector<T>&` parameter, so a
1407
+ // spread of a vector variable (`sum(...arr)`) must pass the vector
1408
+ // directly — NOT `arr.begin(), arr.end()` (which passes two iterators
1409
+ // to one vector param and fails g++). For a non-vector spread we pass
1410
+ // the rendered expression as-is. See demo #6 fix E.
1411
+ argIRs.push(expressionToIR(arg.expression, sourceText, diagnostics, pointerVars));
1412
+ }
1413
+ else {
1414
+ argIRs.push(expressionToIR(arg, sourceText, diagnostics, pointerVars));
1415
+ }
1416
+ }
1417
+ let isStatic = false;
1418
+ let isNamespace = false;
1419
+ if (ts.isPropertyAccessExpression(expr.expression)) {
1420
+ let root = expr.expression;
1421
+ while (ts.isPropertyAccessExpression(root)) {
1422
+ root = root.expression;
1423
+ }
1424
+ if (ts.isIdentifier(root)) {
1425
+ const name = root.text;
1426
+ if (activeNamespaceNames.has(name)) {
1427
+ isNamespace = true;
1428
+ }
1429
+ else if (topLevelClassNames.has(name) || nestedClassAliases.has(name) || name === "Math") {
1430
+ isStatic = true;
1431
+ }
1432
+ }
1433
+ }
1434
+ return {
1435
+ kind: "method-call",
1436
+ callee: calleeText,
1437
+ args: argIRs,
1438
+ isStatic,
1439
+ isNamespace,
1440
+ // isPointer when the callee text already uses ->, OR when the receiver
1441
+ // resolves to a pointer type. The receiver is the object the method is
1442
+ // called on: for `a.b.c()` it is `a.b`. resolveExprCppType walks
1443
+ // class/interface fields to find the receiver's C++ type; if it ends
1444
+ // with '*' (a class-pointer struct field, e.g. FloorState.monster), the
1445
+ // method call must use ->. Without this, `dungeon.monster.bounty()`
1446
+ // emits `.` and fails g++ ("request for member 'bounty' ... pointer
1447
+ // type"). See SUPPORT_MATRIX §4.5 (demo #4 fix).
1448
+ isPointer: calleeText.includes("->")
1449
+ || (ts.isCallExpression(expr)
1450
+ && ts.isPropertyAccessExpression(expr.expression)
1451
+ && (() => {
1452
+ const receiverType = resolveExprCppType(expr.expression.expression);
1453
+ return !!receiverType && parsedIsPointer(receiverType);
1454
+ })()),
1455
+ restElementType: restParamFunctions.get(calleeText),
1456
+ cppType: resolveExprCppType(expr),
1457
+ };
1458
+ }
1459
+ if (ts.isNewExpression(expr)) {
1460
+ let ctorText = formatExpressionText(expr.expression);
1461
+ // Base ctor name without type arguments — used for the Map/Set checks
1462
+ // below, which must fire even when the user wrote `new Map<K, V>()`.
1463
+ const baseCtorName = ctorText;
1464
+ if (expr.typeArguments && expr.typeArguments.length > 0) {
1465
+ // Resolve type args through typeNodeToCppType so `new Registry<string, int32_t>()`
1466
+ // emits `Registry<std::string, int32_t>` (TS type names → C++ type names)
1467
+ // rather than the raw source text or dropping the args entirely.
1468
+ const typeArgs = expr.typeArguments.map((ta) => typeNodeToCppType(ta, undefined)).join(", ");
1469
+ ctorText = `${ctorText}<${typeArgs}>`;
1470
+ }
1471
+ // Special case: new TypedArray([...]) → C++ array initializer
1472
+ const elementType = TYPED_ARRAY_ELEMENT_MAP[ctorText];
1473
+ if (elementType) {
1474
+ const args = expr.arguments ?? [];
1475
+ if (args.length === 1 && ts.isArrayLiteralExpression(args[0])) {
1476
+ const elements = args[0].elements.map(e => expressionToIR(e, sourceText, diagnostics, pointerVars));
1477
+ return { kind: "array", elements, elementType };
1478
+ }
1479
+ // new TypedArray(n) — allocate n elements (zero-initialized)
1480
+ if (args.length === 1) {
1481
+ const sizeIR = expressionToIR(args[0], sourceText, diagnostics, pointerVars);
1482
+ const size = renderExprAsText(sizeIR);
1483
+ const count = parseInt(size, 10);
1484
+ if (!isNaN(count) && count > 0 && count <= 256) {
1485
+ // Return array IR with zero elements so var_decl renderer emits proper C array
1486
+ const zeros = Array(count).fill(0).map(() => ({ kind: "number", value: 0 }));
1487
+ return { kind: "array", elements: zeros, elementType };
1488
+ }
1489
+ // Dynamic size fallback — emit as raw (may not compile in all contexts)
1490
+ return { kind: "raw", value: `{${elementType}(${size})}` };
1491
+ }
1492
+ }
1493
+ const argsText = (expr.arguments ?? [])
1494
+ .map((arg) => {
1495
+ if (ts.isObjectLiteralExpression(arg)) {
1496
+ const fields = [];
1497
+ for (const prop of arg.properties) {
1498
+ if (ts.isPropertyAssignment(prop)) {
1499
+ const name = ts.isIdentifier(prop.name) ? prop.name.text : prop.name.getText();
1500
+ const val = renderExprAsText(expressionToIR(prop.initializer, sourceText, diagnostics, pointerVars));
1501
+ fields.push(`.${name} = ${val}`);
1502
+ }
1503
+ }
1504
+ return fields.length > 0 ? `{ ${fields.join(", ")} }` : "0";
1505
+ }
1506
+ return renderExprAsText(expressionToIR(arg, sourceText, diagnostics, pointerVars));
1507
+ })
1508
+ .join(", ");
1509
+ if (ctorText === "Error") {
1510
+ const message = expr.arguments && expr.arguments.length > 0
1511
+ ? renderExprAsText(expressionToIR(expr.arguments[0], sourceText, diagnostics, pointerVars))
1512
+ : '"error"';
1513
+ return { kind: "raw", value: `std::runtime_error(${message})` };
1514
+ }
1515
+ if (baseCtorName === "Map") {
1516
+ // Demo #30 Finding E — `new Map()` lowers to `{}` (an empty
1517
+ // std::map), but `new Map([[k, v], ...])` (the idiomatic TS
1518
+ // constructor-with-initial-entries form) previously had its argument
1519
+ // DROPPED and also lowered to `{}`, so the map started empty. The fix:
1520
+ // when an initializer argument is present, render it into the
1521
+ // brace-init-list. `argsText` already renders a single array-literal
1522
+ // argument as `{ {k1, v1}, {k2, v2}, ... }` (renderExprAsText of an
1523
+ // array of arrays), which is exactly the `std::initializer_list<pair>`
1524
+ // form `std::map`'s constructor accepts. A non-iterable argument
1525
+ // (`new Map(otherMap)`) has no clean C++ equivalent and falls back to
1526
+ // `{}` — matching the prior behavior. This mirrors the Set fix below.
1527
+ if ((expr.arguments ?? []).length === 1) {
1528
+ return { kind: "raw", value: argsText };
1529
+ }
1530
+ return { kind: "raw", value: "{}" };
1531
+ }
1532
+ if (baseCtorName === "Set") {
1533
+ // Demo #30 Finding E — `new Set()` lowers to `{}` (an empty std::set),
1534
+ // but `new Set([a, b, c])` (the idiomatic TS constructor-with-initial-
1535
+ // elements form) previously had its argument DROPPED and also lowered to
1536
+ // `{}`, so the set started empty. The fix: when an initializer argument
1537
+ // is present, render it into the brace-init-list. `argsText` already
1538
+ // renders a single array-literal argument as `{ a, b, c }`
1539
+ // (renderExprAsText of an array), which is exactly the
1540
+ // `std::initializer_list<T>` form `std::set`'s constructor accepts. A
1541
+ // non-iterable argument (`new Set(otherSet)`) has no clean C++
1542
+ // equivalent and falls back to `{}` — matching the prior behavior.
1543
+ if ((expr.arguments ?? []).length === 1) {
1544
+ return { kind: "raw", value: argsText };
1545
+ }
1546
+ return { kind: "raw", value: "{}" };
1547
+ }
1548
+ // `new Array<E>(...)` — the idiomatic TS pre-sized/empty array constructor.
1549
+ // Demo #29 Finding B: this was previously emitted verbatim
1550
+ // (`new Array<uint32_t>(256)`) and failed at g++ time ("'Array' does not
1551
+ // name a type"), because the `Array` constructor — unlike `Array.from`/
1552
+ // `Array.of` (which are intentionally rejected) — had neither a lowering
1553
+ // nor a gate. The clean C++ equivalent is a `std::vector<E>`:
1554
+ // `new Array<E>(n)` → `std::vector<E>(n)` (n value-initialized elems)
1555
+ // `new Array<E>()` → `std::vector<E>()` (empty)
1556
+ // `new Array<E>(a,b,c)` → `std::vector<E>{a,b,c}` (element list)
1557
+ // The element type comes from the type argument; without one we cannot
1558
+ // pick a C++ element type, so fall through to the unsupported-ctor path
1559
+ // (gated by feature-registry) rather than guess.
1560
+ if (baseCtorName === "Array" && expr.typeArguments && expr.typeArguments.length === 1) {
1561
+ const elemCpp = typeNodeToCppType(expr.typeArguments[0], undefined);
1562
+ const args = expr.arguments ?? [];
1563
+ if (args.length === 0) {
1564
+ return { kind: "raw", value: `std::vector<${elemCpp}>()` };
1565
+ }
1566
+ if (args.length === 1) {
1567
+ // Sized constructor: n value-initialized elements. The size argument
1568
+ // is rendered through the normal expression path.
1569
+ const sizeText = renderExprAsText(expressionToIR(args[0], sourceText, diagnostics, pointerVars));
1570
+ return { kind: "raw", value: `std::vector<${elemCpp}>(${sizeText})` };
1571
+ }
1572
+ // Two+ args: treat as an element list (TS `new Array<E>(a, b, c)`).
1573
+ return { kind: "raw", value: `std::vector<${elemCpp}>{ ${argsText} }` };
1574
+ }
1575
+ const resolvedCtorText = nestedClassAliases.get(ctorText) ?? ctorText;
1576
+ // Tag the node with the constructed class name so the heap-allocation
1577
+ // validator can detect heap allocation by CONSTRUCT (demo #34 Finding A),
1578
+ // not by pattern-matching the `raw` text (whose presence depended on
1579
+ // unrelated import structure).
1580
+ return { kind: "raw", value: `new ${resolvedCtorText}(${argsText})`, newClassName: resolvedCtorText };
1581
+ }
1582
+ if (ts.isStringLiteral(expr)) {
1583
+ return { kind: "string", value: expr.text };
1584
+ }
1585
+ // Handle template literals without interpolation (simple strings)
1586
+ if (ts.isNoSubstitutionTemplateLiteral(expr)) {
1587
+ return { kind: "string", value: expr.text };
1588
+ }
1589
+ // Handle template literals with interpolation - convert to string concatenation
1590
+ if (ts.isTemplateExpression(expr)) {
1591
+ // Build a string concatenation expression from the template literal
1592
+ const parts = [];
1593
+ // Add the head text (before first interpolation)
1594
+ if (expr.head.text) {
1595
+ parts.push({ kind: "string", value: expr.head.text });
1596
+ }
1597
+ // Add each template span (interpolation + trailing text)
1598
+ for (const span of expr.templateSpans) {
1599
+ // Add the interpolated expression (converted to string if needed)
1600
+ const exprIR = expressionToIR(span.expression, sourceText, diagnostics, pointerVars);
1601
+ // Wrap in a template_string conversion - the emitter will handle toString conversion
1602
+ parts.push({ kind: "template_string", expression: exprIR });
1603
+ // Add the trailing text
1604
+ if (span.literal.text) {
1605
+ parts.push({ kind: "string", value: span.literal.text });
1606
+ }
1607
+ }
1608
+ // If only one part, return it directly
1609
+ if (parts.length === 1) {
1610
+ return parts[0];
1611
+ }
1612
+ // Build concatenation chain
1613
+ return { kind: "string_concat", parts };
1614
+ }
1615
+ if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword) {
1616
+ return { kind: "boolean", value: expr.kind === ts.SyntaxKind.TrueKeyword };
1617
+ }
1618
+ // Handle null keyword
1619
+ if (expr.kind === ts.SyntaxKind.NullKeyword) {
1620
+ return { kind: "identifier", value: "nullptr" };
1621
+ }
1622
+ if (expr.kind === ts.SyntaxKind.UndefinedKeyword) {
1623
+ return { kind: "identifier", value: "CUTTLEFISH_UNDEFINED" };
1624
+ }
1625
+ if (ts.isIdentifier(expr)) {
1626
+ // Resolve tracked HAL instances to their resolved values
1627
+ const halInst = halInstances.get(expr.text);
1628
+ if (halInst) {
1629
+ // For Pin instances, resolve to the pin number
1630
+ if (halInst.fieldValues.has("_pin")) {
1631
+ return { kind: "raw", value: halInst.fieldValues.get("_pin") };
1632
+ }
1633
+ }
1634
+ // Apply nested-function-alias mangling so a `return dbl` reference to a
1635
+ // nested function declaration resolves to its hoisted mangled name
1636
+ // (makeScaler__dbl). Without this, the reference emits the bare name and
1637
+ // g++ reports "'dbl' was not declared in scope" (demo #9 Finding B).
1638
+ const nestedAlias = nestedFunctionAliases.get(expr.text);
1639
+ return { kind: "identifier", value: nestedAlias ?? expr.text };
1640
+ }
1641
+ // Handle 'this' keyword
1642
+ if (expr.kind === ts.SyntaxKind.ThisKeyword) {
1643
+ return { kind: "raw", value: "this" };
1644
+ }
1645
+ // Handle property access expressions like obj.property or this.field
1646
+ if (ts.isPropertyAccessExpression(expr)) {
1647
+ // ── Ambient canvas dims: ctx.width / ctx.height → __ui_canvas_w / _h ──
1648
+ {
1649
+ const canvasCtx = getCanvasAmbientCtx();
1650
+ if (canvasCtx &&
1651
+ ts.isIdentifier(expr.expression) &&
1652
+ expr.expression.text === canvasCtx &&
1653
+ (expr.name.text === "width" || expr.name.text === "height")) {
1654
+ return {
1655
+ kind: "identifier",
1656
+ value: expr.name.text === "width" ? "__ui_canvas_w" : "__ui_canvas_h",
1657
+ };
1658
+ }
1659
+ }
1660
+ // ── UI element .value read: screen.led.value → __ui_nodes[N].value ──
1661
+ if (expr.name.text === "value" &&
1662
+ ts.isPropertyAccessExpression(expr.expression) &&
1663
+ ts.isIdentifier(expr.expression.expression)) {
1664
+ const treeName = expr.expression.expression.text;
1665
+ const elemId = expr.expression.name.text;
1666
+ const nodeIdx = resolveElementValue(treeName, elemId);
1667
+ if (nodeIdx !== undefined) {
1668
+ return { kind: "raw", value: `__ui_nodes[${nodeIdx}].value` };
1669
+ }
1670
+ }
1671
+ // ── UI element .text read: screen.ssid.text → __ui_nodes[N].textBuffer ──
1672
+ if (expr.name.text === "text" &&
1673
+ ts.isPropertyAccessExpression(expr.expression) &&
1674
+ ts.isIdentifier(expr.expression.expression)) {
1675
+ const treeName = expr.expression.expression.text;
1676
+ const elemId = expr.expression.name.text;
1677
+ const nodeIdx = resolveElementValue(treeName, elemId);
1678
+ if (nodeIdx !== undefined) {
1679
+ return { kind: "raw", value: `__ui_nodes[${nodeIdx}].textBuffer` };
1680
+ }
1681
+ }
1682
+ const propName = expr.name.kind === ts.SyntaxKind.PrivateIdentifier
1683
+ ? `__priv_${expr.name.text.substring(1)}`
1684
+ : expr.name.text;
1685
+ if (isOptionalChainNode(expr)) {
1686
+ diagnostics.push(makeDiagnostic(sourceText, expr.pos, "Optional chaining (?.) is lowered with an approximate null guard in C++; semantics may differ from TypeScript.", "warning", "TS2CPP_OPTIONAL_CHAINING"));
1687
+ const accessText = renderMemberAccessText(expr.expression, propName);
1688
+ return { kind: "raw", value: renderOptionalGuardedAccess(expr.expression, accessText) };
1689
+ }
1690
+ const regRead = tryLowerRegisterRead(expr, sourceText, diagnostics);
1691
+ if (regRead !== null) {
1692
+ return regRead;
1693
+ }
1694
+ if (expr.expression.kind === ts.SyntaxKind.ThisKeyword) {
1695
+ return { kind: "property-access", object: { kind: "raw", value: "this" }, property: propName };
1696
+ }
1697
+ if (ts.isIdentifier(expr.expression) && expr.expression.text === "Math") {
1698
+ if (propName === "random")
1699
+ return { kind: "raw", value: "__tc_random()" };
1700
+ const mathConst = MATH_CONSTANT_LITERALS[propName];
1701
+ if (mathConst !== undefined)
1702
+ return { kind: "number", value: mathConst };
1703
+ return { kind: "raw", value: `std::${propName}` };
1704
+ }
1705
+ const object = expressionToIR(expr.expression, sourceText, diagnostics, pointerVars);
1706
+ if (propName === "length") {
1707
+ const objectText = renderExprAsText(object);
1708
+ const resolved = resolveLengthProperty(expr.expression, objectText);
1709
+ if (resolved.startsWith("__FILTERED_LEN__")) {
1710
+ return { kind: "identifier", value: resolved.slice("__FILTERED_LEN__".length) };
1711
+ }
1712
+ return { kind: "raw", value: resolved };
1713
+ }
1714
+ // `.size` on a Map/Set (std::map/std::set) — these expose size as a method
1715
+ // (`m.size()`), not a member. Map it to a method call so it doesn't fall
1716
+ // through to the pointer-deref property-access path (which would emit
1717
+ // `m->size` / `this->field.size`). Mirrors the `.length` → `.size()`
1718
+ // lowering for vectors.
1719
+ //
1720
+ // Demo #29 Finding C: this previously resolved the receiver type ONLY for a
1721
+ // bare identifier, so `this.field.size` / `obj.field.size` on a Map/Set
1722
+ // field fell through to the generic property-access path and emitted the
1723
+ // bare member `this->field.size` (g++: "has no member named 'size'"). The
1724
+ // `.length` path (resolveLengthProperty) was already extended to member
1725
+ // receivers in demos #22/#27; this closes the parallel `.size` gap. We now
1726
+ // resolve the receiver via the shared `resolveExprCppType`, which covers
1727
+ // bare identifiers, `this.field`, `obj.field`, and element access uniformly.
1728
+ if (propName === "size") {
1729
+ const receiverType = resolveExprCppType(expr.expression);
1730
+ if (receiverType && (parsedIsMap(receiverType) || parsedIsSet(receiverType))) {
1731
+ const objectText = renderExprAsText(object);
1732
+ return { kind: "raw", value: `static_cast<long long>(${objectText}.size())` };
1733
+ }
1734
+ }
1735
+ // Use -> for pointer variables in property access
1736
+ if (ts.isIdentifier(expr.expression) && (pointerVars.has(expr.expression.text) || parsedIsPointer(getCurrentIrTypeScope()?.locals.get(expr.expression.text) ?? "") || parsedIsPointer(getCurrentIrTypeScope()?.globals.get(expr.expression.text) ?? ""))) {
1737
+ return {
1738
+ kind: "property-access",
1739
+ object: { kind: "identifier", value: expr.expression.text },
1740
+ property: propName,
1741
+ isPointer: true
1742
+ };
1743
+ }
1744
+ // Use -> for this->pointerField in property access (e.g. this.player.name)
1745
+ if (ts.isPropertyAccessExpression(expr.expression)
1746
+ && expr.expression.expression.kind === ts.SyntaxKind.ThisKeyword
1747
+ && ts.isIdentifier(expr.expression.name)) {
1748
+ const fieldName = expr.expression.name.text;
1749
+ const fieldType = getCurrentIrTypeScope()?.locals.get(`this->${fieldName}`);
1750
+ if (fieldType && parsedIsPointer(fieldType)) {
1751
+ return {
1752
+ kind: "property-access",
1753
+ object: expressionToIR(expr.expression, sourceText, diagnostics, pointerVars),
1754
+ property: propName,
1755
+ isPointer: true,
1756
+ };
1757
+ }
1758
+ }
1759
+ // General deep chain: resolve receiver type, use -> if receiver is a pointer
1760
+ const receiverCppType = resolveExprCppType(expr.expression);
1761
+ if (receiverCppType && parsedIsPointer(receiverCppType)) {
1762
+ return {
1763
+ kind: "property-access",
1764
+ object,
1765
+ property: propName,
1766
+ isPointer: true,
1767
+ };
1768
+ }
1769
+ let isEnum = false;
1770
+ let isNamespace = false;
1771
+ let isStatic = false;
1772
+ if (ts.isIdentifier(expr.expression)) {
1773
+ const name = expr.expression.text;
1774
+ if (activeEnumNames.has(name)) {
1775
+ isEnum = true;
1776
+ }
1777
+ else if (activeNamespaceNames.has(name)) {
1778
+ isNamespace = true;
1779
+ }
1780
+ else if (topLevelClassNames.has(name) || nestedClassAliases.has(name)) {
1781
+ isStatic = true;
1782
+ }
1783
+ }
1784
+ else if (ts.isPropertyAccessExpression(expr.expression)) {
1785
+ let root = expr.expression;
1786
+ while (ts.isPropertyAccessExpression(root)) {
1787
+ root = root.expression;
1788
+ }
1789
+ if (ts.isIdentifier(root) && activeNamespaceNames.has(root.text)) {
1790
+ isNamespace = true;
1791
+ }
1792
+ }
1793
+ let isPointer = false;
1794
+ if (ts.isIdentifier(expr.expression) && pointerVars.has(expr.expression.text)) {
1795
+ isPointer = true;
1796
+ }
1797
+ else if (expr.expression.kind === ts.SyntaxKind.ThisKeyword) {
1798
+ isPointer = true;
1799
+ }
1800
+ return {
1801
+ kind: "property-access",
1802
+ object,
1803
+ property: propName,
1804
+ isEnum,
1805
+ isNamespace,
1806
+ isStatic,
1807
+ isPointer
1808
+ };
1809
+ }
1810
+ if (ts.isElementAccessExpression(expr)) {
1811
+ const object = expressionToIR(expr.expression, sourceText, diagnostics, pointerVars);
1812
+ const index = expressionToIR(expr.argumentExpression, sourceText, diagnostics, pointerVars);
1813
+ if (index.kind === "number" && Number.isInteger(index.value)) {
1814
+ let objectType;
1815
+ let elementType;
1816
+ if (ts.isIdentifier(expr.expression)) {
1817
+ objectType = getCurrentIrTypeScope()?.locals.get(expr.expression.text) ?? getCurrentIrTypeScope()?.globals.get(expr.expression.text);
1818
+ if (objectType) {
1819
+ elementType = parsedElementString(objectType);
1820
+ }
1821
+ }
1822
+ else if (ts.isPropertyAccessExpression(expr.expression)) {
1823
+ const receiverNode = expr.expression.expression;
1824
+ if (ts.isIdentifier(receiverNode)) {
1825
+ const receiverType = getCurrentIrTypeScope()?.locals.get(receiverNode.text) ?? getCurrentIrTypeScope()?.globals.get(receiverNode.text);
1826
+ if (receiverType) {
1827
+ const className = parsedBareString(receiverType);
1828
+ const classDef = topLevelClasses.get(className);
1829
+ if (classDef) {
1830
+ const fieldName = expr.expression.name.text;
1831
+ const field = classDef.fields.find((f) => f.name === fieldName);
1832
+ if (field) {
1833
+ objectType = field.cppType;
1834
+ elementType = parsedElementString(field.cppType);
1835
+ }
1836
+ }
1837
+ }
1838
+ }
1839
+ }
1840
+ if (typeof objectType === "string" && parsedIsTuple(objectType)) {
1841
+ return { kind: "tuple-access", object, index: index.value };
1842
+ }
1843
+ return { kind: "element-access", object, index, elementType };
1844
+ }
1845
+ return { kind: "element-access", object, index };
1846
+ }
1847
+ // Handle ternary/conditional expressions: a ? b : c
1848
+ if (ts.isConditionalExpression(expr)) {
1849
+ return {
1850
+ kind: "ternary",
1851
+ condition: expressionToIR(expr.condition, sourceText, diagnostics),
1852
+ whenTrue: expressionToIR(expr.whenTrue, sourceText, diagnostics),
1853
+ whenFalse: expressionToIR(expr.whenFalse, sourceText, diagnostics),
1854
+ };
1855
+ }
1856
+ // Handle array literals
1857
+ if (ts.isArrayLiteralExpression(expr)) {
1858
+ // Check for spread element in array
1859
+ const spreadIndex = expr.elements.findIndex(e => ts.isSpreadElement(e));
1860
+ if (spreadIndex !== -1) {
1861
+ // Handle spread in array: [...arr, x, y]
1862
+ const spreadElement = expr.elements[spreadIndex];
1863
+ if (ts.isSpreadElement(spreadElement)) {
1864
+ // If spread source is a known-size array variable, expand inline
1865
+ if (ts.isIdentifier(spreadElement.expression) && arrayLiteralSizes.has(spreadElement.expression.text)) {
1866
+ const srcName = spreadElement.expression.text;
1867
+ const srcSize = arrayLiteralSizes.get(srcName);
1868
+ const expandedElements = [];
1869
+ for (let i = 0; i < srcSize; i++) {
1870
+ expandedElements.push({ kind: "raw", value: `${srcName}[${i}]` });
1871
+ }
1872
+ for (const elem of expr.elements.slice(spreadIndex + 1)) {
1873
+ expandedElements.push(expressionToIR(elem, sourceText, diagnostics));
1874
+ }
1875
+ return { kind: "array", elementType: "auto", elements: expandedElements };
1876
+ }
1877
+ const spreadExpr = expressionToIR(spreadElement.expression, sourceText, diagnostics);
1878
+ const additionalElements = expr.elements
1879
+ .slice(spreadIndex + 1)
1880
+ .map(e => expressionToIR(e, sourceText, diagnostics));
1881
+ // Emit as spread_array IR node
1882
+ return {
1883
+ kind: "spread_array",
1884
+ elementType: "auto",
1885
+ spreadExpr,
1886
+ additionalElements
1887
+ };
1888
+ }
1889
+ }
1890
+ const nonOmittedElements = expr.elements.filter(e => e.kind !== ts.SyntaxKind.OmittedExpression);
1891
+ const elements = nonOmittedElements
1892
+ .map((e) => expressionToIR(e, sourceText, diagnostics));
1893
+ // Resolve the element cppType when possible so downstream renderers can emit
1894
+ // a TYPED `std::vector<ElemType>{...}`. This matters when an inline array
1895
+ // literal is the receiver of a `__tc_*` template helper
1896
+ // (`[...].join(sep)`, `[...].concat(x)`, ...): a bare `{...}` cannot drive
1897
+ // template argument deduction, but `std::vector<std::string>{...}` can.
1898
+ // Infer from the first element's resolved cppType; fall back to `"auto"`
1899
+ // (the historical default) when no element carries a type. Demo #29 Finding D.
1900
+ const elementType = inferArrayElementType(nonOmittedElements);
1901
+ return { kind: "array", elementType, elements };
1902
+ }
1903
+ // Handle object literals - suppress warning for compile-time type contexts
1904
+ // Object literals in board package files are often type-asserted to pin interfaces
1905
+ // These are compile-time constructs that don't need C++ emission
1906
+ if (ts.isObjectLiteralExpression(expr)) {
1907
+ const fields = [];
1908
+ const spreadSources = [];
1909
+ for (const prop of expr.properties) {
1910
+ if (ts.isPropertyAssignment(prop)) {
1911
+ const name = ts.isIdentifier(prop.name) ? prop.name.text : prop.name.getText();
1912
+ fields.push({
1913
+ name,
1914
+ value: expressionToIR(prop.initializer, sourceText, diagnostics, pointerVars),
1915
+ });
1916
+ }
1917
+ else if (ts.isShorthandPropertyAssignment(prop)) {
1918
+ fields.push({
1919
+ name: prop.name.text,
1920
+ value: { kind: "identifier", value: prop.name.text },
1921
+ });
1922
+ }
1923
+ else if (ts.isMethodDeclaration(prop)) {
1924
+ const name = ts.isIdentifier(prop.name) ? prop.name.text : prop.name.getText();
1925
+ fields.push({
1926
+ name,
1927
+ value: { kind: "raw", value: "/* method stub */" },
1928
+ });
1929
+ }
1930
+ else if (ts.isAccessor(prop)) {
1931
+ const name = ts.isIdentifier(prop.name) ? prop.name.text : prop.name.getText();
1932
+ fields.push({
1933
+ name,
1934
+ value: { kind: "raw", value: "/* accessor stub */" },
1935
+ });
1936
+ }
1937
+ else if (ts.isSpreadAssignment(prop)) {
1938
+ spreadSources.push(expressionToIR(prop.expression, sourceText, diagnostics, pointerVars));
1939
+ }
1940
+ }
1941
+ if (fields.length === 0 && spreadSources.length > 0) {
1942
+ const allFields = [];
1943
+ for (const src of spreadSources) {
1944
+ allFields.push({ name: "__spread__", value: src });
1945
+ }
1946
+ return { kind: "object", fields: allFields };
1947
+ }
1948
+ if (spreadSources.length > 0 && fields.length > 0) {
1949
+ const allFields = [];
1950
+ for (const src of spreadSources) {
1951
+ const srcText = renderExprAsText(src);
1952
+ allFields.push({ name: "__spread__", value: src });
1953
+ }
1954
+ allFields.push(...fields);
1955
+ return { kind: "object", fields: allFields };
1956
+ }
1957
+ return { kind: "object", fields };
1958
+ }
1959
+ // Handle function expressions and arrow functions in compile-time contexts
1960
+ // These are stubs in board package files that get replaced by transpiler magic
1961
+ // For interrupt handlers, we need to generate a proper callback function
1962
+ if (ts.isFunctionExpression(expr) || ts.isArrowFunction(expr)) {
1963
+ const body = expr.body;
1964
+ // Resolve each parameter's C++ type from its type annotation rather than
1965
+ // hardcoding "auto". This matters for callbacks hoisted to free functions
1966
+ // (top-level-prep.ts collectCallbackFromExpression): the ISR signature is
1967
+ // rendered from these cppTypes, and an "auto" param is filtered out, so a
1968
+ // `.map((n) => ...)` callback would lower to `void X_isr_N()` (no params)
1969
+ // and the __tc_map template couldn't deduce the element/result types.
1970
+ // Default to "auto" only when the param has no annotation.
1971
+ const paramList = [];
1972
+ const lambdaLocalTypes = new Map();
1973
+ for (const param of expr.parameters) {
1974
+ if (ts.isIdentifier(param.name)) {
1975
+ const resolved = typeNodeToCppType(param.type, undefined);
1976
+ const cppType = resolved && resolved !== "auto" ? resolved : "auto";
1977
+ paramList.push({
1978
+ name: param.name.text,
1979
+ cppType,
1980
+ });
1981
+ if (cppType !== "auto") {
1982
+ lambdaLocalTypes.set(param.name.text, cppType);
1983
+ }
1984
+ }
1985
+ }
1986
+ const isBlock = ts.isBlock(body);
1987
+ const bodyStmts = isBlock
1988
+ ? body.statements.map(stmt => {
1989
+ const lowered = lowerStatement(stmt, "", sourceText, diagnostics, new Map(), new Map(), "<lambda>", new Map(), pointerVars);
1990
+ const stmts = lowered ?? [{ kind: "call", sourceSpan: makeSourceSpan(stmt, "", sourceText), callee: "__EMIT__", args: [{ kind: "string", value: "" }] }];
1991
+ return stmts;
1992
+ }).flat()
1993
+ : [{
1994
+ kind: "return",
1995
+ sourceSpan: makeSourceSpan(body, "", sourceText),
1996
+ value: expressionToIR(body, sourceText, diagnostics, pointerVars),
1997
+ }];
1998
+ // Infer return type: use explicit annotation, or infer from body. Thread
1999
+ // the lambda's own param types into the inference so a body like
2000
+ // `(n) => n.capacity` can resolve `n` and deduce the return type.
2001
+ // When the body returns a value but the type can't be resolved (common when
2002
+ // the param is an interface, whose fields aren't in the class registry),
2003
+ // fall back to "auto" (C++14 return-type deduction) rather than "void" —
2004
+ // this lets the __tc_map/__tc_filter template helpers deduce the result
2005
+ // type via decltype(fn(v[0])) instead of failing on a void(auto) callable.
2006
+ let inferredReturnType;
2007
+ const explicitReturnType = typeNodeToCppType(expr.type, undefined);
2008
+ if (explicitReturnType !== "auto" && explicitReturnType !== "void") {
2009
+ inferredReturnType = explicitReturnType;
2010
+ }
2011
+ else if (isBlock) {
2012
+ const returns = collectReturns(body).filter((item) => item.expression);
2013
+ const returnTypes = returns
2014
+ .map((item) => inferExprCppType(item.expression, new Map(), lambdaLocalTypes, sourceText))
2015
+ .filter((item) => item !== "auto");
2016
+ if (returnTypes.includes("float") || returnTypes.includes("double")) {
2017
+ inferredReturnType = "double";
2018
+ }
2019
+ else if (returnTypes.includes("std::string")) {
2020
+ inferredReturnType = "std::string";
2021
+ }
2022
+ else if (returnTypes.includes("int") || returnTypes.includes("bool")) {
2023
+ inferredReturnType = "int";
2024
+ }
2025
+ else if (returnTypes.length > 0) {
2026
+ inferredReturnType = returnTypes[0];
2027
+ }
2028
+ else {
2029
+ // Body has returns but the types didn't resolve (e.g. interface-typed
2030
+ // param). Use C++14 auto return deduction so the callback is callable.
2031
+ inferredReturnType = returns.length > 0 ? "auto" : "void";
2032
+ }
2033
+ }
2034
+ else {
2035
+ inferredReturnType = inferExprCppType(body, new Map(), lambdaLocalTypes, sourceText);
2036
+ if (inferredReturnType === "auto") {
2037
+ // Expression body whose type didn't resolve — still prefer auto over
2038
+ // void so the value is returned (not dropped).
2039
+ inferredReturnType = "auto";
2040
+ }
2041
+ }
2042
+ const lambdaParams = paramList.map(p => ({ name: p.name, cppType: p.cppType }));
2043
+ return { kind: "lambda", params: lambdaParams, body: bodyStmts, returnType: inferredReturnType, isExpressionBody: !isBlock };
2044
+ }
2045
+ // Handle instanceof expressions
2046
+ if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) {
2047
+ const object = expressionToIR(expr.left, sourceText, diagnostics);
2048
+ const className = expr.right.getText();
2049
+ return { kind: "instanceof", object, className };
2050
+ }
2051
+ // Handle class expressions (anonymous classes assigned to variables)
2052
+ if (ts.isClassExpression(expr)) {
2053
+ const className = expr.name ? expr.name.text : `__tc_anonClass`;
2054
+ const members = [];
2055
+ let ctor;
2056
+ const fields = [];
2057
+ const methods = [];
2058
+ const getters = [];
2059
+ const setters = [];
2060
+ for (const member of expr.members) {
2061
+ if (ts.isConstructorDeclaration(member)) {
2062
+ const ctorParams = [];
2063
+ for (const param of member.parameters) {
2064
+ if (ts.isIdentifier(param.name)) {
2065
+ ctorParams.push({ name: param.name.text, cppType: "auto", isRest: !!param.dotDotDotToken });
2066
+ }
2067
+ }
2068
+ const ctorBody = member.body
2069
+ ? [...member.body.statements].map(s => {
2070
+ const lowered = lowerStatement(s, "", sourceText, diagnostics, new Map(), new Map(), "<anon-class>", new Map(), pointerVars);
2071
+ return lowered ?? [];
2072
+ }).flat()
2073
+ : [];
2074
+ ctor = { parameters: ctorParams, statements: ctorBody };
2075
+ continue;
2076
+ }
2077
+ if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
2078
+ const methodBody = member.body
2079
+ ? [...member.body.statements].map(s => {
2080
+ const lowered = lowerStatement(s, "", sourceText, diagnostics, new Map(), new Map(), `<anon-class>.${member.name.getText()}`, new Map(), pointerVars);
2081
+ return lowered ?? [];
2082
+ }).flat()
2083
+ : [];
2084
+ methods.push({
2085
+ name: member.name.text,
2086
+ returnType: "auto",
2087
+ parameters: [],
2088
+ statements: methodBody,
2089
+ visibility: "public",
2090
+ isStatic: false,
2091
+ isAbstract: false,
2092
+ });
2093
+ continue;
2094
+ }
2095
+ if (ts.isPropertyDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
2096
+ fields.push({
2097
+ name: member.name.text,
2098
+ cppType: "auto",
2099
+ visibility: "public",
2100
+ initializer: member.initializer ? expressionToIR(member.initializer, sourceText, diagnostics, pointerVars) : undefined,
2101
+ });
2102
+ continue;
2103
+ }
2104
+ if (ts.isGetAccessorDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
2105
+ const getterBody = member.body
2106
+ ? [...member.body.statements].map(s => {
2107
+ const lowered = lowerStatement(s, "", sourceText, diagnostics, new Map(), new Map(), `<anon-class>.get:${member.name.getText()}`, new Map(), pointerVars);
2108
+ return lowered ?? [];
2109
+ }).flat()
2110
+ : [];
2111
+ getters.push({
2112
+ name: member.name.text,
2113
+ returnType: "auto",
2114
+ statements: getterBody,
2115
+ visibility: "public",
2116
+ isStatic: false,
2117
+ });
2118
+ continue;
2119
+ }
2120
+ if (ts.isSetAccessorDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
2121
+ const setterBody = member.body
2122
+ ? [...member.body.statements].map(s => {
2123
+ const lowered = lowerStatement(s, "", sourceText, diagnostics, new Map(), new Map(), `<anon-class>.set:${member.name.getText()}`, new Map(), pointerVars);
2124
+ return lowered ?? [];
2125
+ }).flat()
2126
+ : [];
2127
+ setters.push({
2128
+ name: member.name.text,
2129
+ parameter: { name: "value", cppType: "auto", isRest: false },
2130
+ statements: setterBody,
2131
+ visibility: "public",
2132
+ isStatic: false,
2133
+ });
2134
+ continue;
2135
+ }
2136
+ }
2137
+ const syntheticSpan = { filePath: "", startOffset: 0, endOffset: 0, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0 };
2138
+ hoistedNestedClasses.push({
2139
+ name: className,
2140
+ isAbstract: false,
2141
+ sourceSpan: syntheticSpan,
2142
+ fields,
2143
+ methods,
2144
+ getters,
2145
+ setters,
2146
+ ...(ctor ? { constructor: ctor } : {}),
2147
+ });
2148
+ topLevelClassNames.add(className);
2149
+ classTypeNames.add(className);
2150
+ const extendsClass = expr.heritageClauses
2151
+ ?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)
2152
+ ?.types[0]?.expression?.getText();
2153
+ if (extendsClass) {
2154
+ hoistedNestedClasses[hoistedNestedClasses.length - 1].extendsClass = extendsClass;
2155
+ }
2156
+ return { kind: "identifier", value: className };
2157
+ }
2158
+ // Handle delete expressions: delete obj.key → map.erase(key)
2159
+ if (ts.isDeleteExpression(expr)) {
2160
+ const target = expr.expression;
2161
+ if (ts.isPropertyAccessExpression(target)) {
2162
+ const keyText = target.name.text;
2163
+ const objNode = target.expression;
2164
+ const objText = renderExprAsText(expressionToIR(objNode, sourceText, diagnostics, pointerVars));
2165
+ let objType;
2166
+ if (ts.isIdentifier(objNode)) {
2167
+ objType = getCurrentIrTypeScope()?.locals.get(objNode.text) ?? getCurrentIrTypeScope()?.globals.get(objNode.text);
2168
+ }
2169
+ if (objType && (parsedIsMap(objType) || parsedIsSet(objType))) {
2170
+ return { kind: "raw", value: `(${objText}.erase(${keyText}), true)` };
2171
+ }
2172
+ if (objType && parsedIsVector(objType)) {
2173
+ const elemText = renderExprAsText(expressionToIR(target, sourceText, diagnostics, pointerVars));
2174
+ return { kind: "raw", value: `(${objText}.erase(std::find(${objText}.begin(), ${objText}.end(), ${keyText})), true)` };
2175
+ }
2176
+ }
2177
+ if (ts.isElementAccessExpression(target)) {
2178
+ const keyIR = expressionToIR(target.argumentExpression, sourceText, diagnostics, pointerVars);
2179
+ const objNode = target.expression;
2180
+ const objText = renderExprAsText(expressionToIR(objNode, sourceText, diagnostics, pointerVars));
2181
+ const keyText = renderExprAsText(keyIR);
2182
+ let objType;
2183
+ if (ts.isIdentifier(objNode)) {
2184
+ objType = getCurrentIrTypeScope()?.locals.get(objNode.text) ?? getCurrentIrTypeScope()?.globals.get(objNode.text);
2185
+ }
2186
+ if (objType && parsedIsMap(objType)) {
2187
+ return { kind: "raw", value: `(${objText}.erase(${keyText}) > 0)` };
2188
+ }
2189
+ }
2190
+ if (ts.isPropertyAccessExpression(target)) {
2191
+ const keyText = target.name.text;
2192
+ const objNode = target.expression;
2193
+ const objText = renderExprAsText(expressionToIR(objNode, sourceText, diagnostics, pointerVars));
2194
+ let objType;
2195
+ if (ts.isIdentifier(objNode)) {
2196
+ objType = getCurrentIrTypeScope()?.locals.get(objNode.text) ?? getCurrentIrTypeScope()?.globals.get(objNode.text);
2197
+ }
2198
+ if (objType) {
2199
+ const fieldTypeName = objType;
2200
+ let defaultValue = "0";
2201
+ if (fieldTypeName === "std::string" || fieldTypeName === "const char*")
2202
+ defaultValue = "\"\"";
2203
+ else if (fieldTypeName === "bool")
2204
+ defaultValue = "false";
2205
+ else if (parsedIsPointer(fieldTypeName))
2206
+ defaultValue = "nullptr";
2207
+ return { kind: "raw", value: `(${objText}.${keyText} = ${defaultValue}, true)` };
2208
+ }
2209
+ }
2210
+ return emitUnsupportedExpression("delete on non-map types is unsupported in C++.");
2211
+ }
2212
+ // Handle comma expressions: (a, b, c) → evaluate all, return last
2213
+ if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.CommaToken) {
2214
+ const leftIR = expressionToIR(expr.left, sourceText, diagnostics, pointerVars);
2215
+ const rightIR = expressionToIR(expr.right, sourceText, diagnostics, pointerVars);
2216
+ const leftText = renderExprAsText(leftIR);
2217
+ const rightText = renderExprAsText(rightIR);
2218
+ return { kind: "raw", value: `(${leftText}, ${rightText})` };
2219
+ }
2220
+ // Handle tagged template expressions (e.g., tag`template`)
2221
+ if (ts.isTaggedTemplateExpression(expr)) {
2222
+ return emitUnsupportedExpression("Tagged template expressions are unsupported in the C++ transpiler and were lowered to a placeholder.");
2223
+ }
2224
+ // Handle meta properties (e.g., import.meta, new.target)
2225
+ if (ts.isMetaProperty(expr)) {
2226
+ return emitUnsupportedExpression("Meta-property expressions are unsupported in the C++ transpiler and were lowered to a placeholder.");
2227
+ }
2228
+ if (ts.isNonNullExpression(expr)) {
2229
+ return expressionToIR(expr.expression, sourceText, diagnostics, pointerVars);
2230
+ }
2231
+ if (expr.kind === ts.SyntaxKind.SuperKeyword) {
2232
+ const extendsClass = getActiveExtendsClass();
2233
+ if (extendsClass) {
2234
+ return { kind: "raw", value: extendsClass };
2235
+ }
2236
+ return emitUnsupportedExpression("super keyword outside of class method");
2237
+ }
2238
+ if (ts.isPartiallyEmittedExpression(expr)) {
2239
+ return expressionToIR(expr.expression, sourceText, diagnostics, pointerVars);
2240
+ }
2241
+ if (expr.kind === ts.SyntaxKind.YieldExpression || ts.isYieldExpression?.(expr)) {
2242
+ const yieldExpr = expr;
2243
+ const value = yieldExpr.expression
2244
+ ? expressionToIR(yieldExpr.expression, sourceText, diagnostics, pointerVars)
2245
+ : undefined;
2246
+ return { kind: "raw", value: value ? `co_yield ${renderExprAsText(value)}` : "co_yield" };
2247
+ }
2248
+ return emitUnsupportedExpression(`Unsupported expression kind '${ts.SyntaxKind[expr.kind] ?? expr.kind}' was lowered to a placeholder.`);
2249
+ }