@scriptc/compiler 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (588) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +7 -18
  2. package/dist/backend/cycle-analysis.d.ts +21 -9
  3. package/dist/backend/cycle-analysis.js +139 -67
  4. package/dist/backend/cycle-analysis.js.map +1 -1
  5. package/dist/backend/cycle-analysis.test.js +283 -3
  6. package/dist/backend/cycle-analysis.test.js.map +1 -1
  7. package/dist/backend/cycle-mutability.d.ts +39 -0
  8. package/dist/backend/cycle-mutability.js +205 -0
  9. package/dist/backend/cycle-mutability.js.map +1 -0
  10. package/dist/backend/link-plan-core.d.ts +2 -1
  11. package/dist/backend/link-plan-core.js +15 -3
  12. package/dist/backend/link-plan-core.js.map +1 -1
  13. package/dist/backend/link-plan.d.ts +2 -1
  14. package/dist/backend/link-plan.js.map +1 -1
  15. package/dist/backend/llvm/alloc.d.ts +43 -0
  16. package/dist/backend/llvm/alloc.js +246 -0
  17. package/dist/backend/llvm/alloc.js.map +1 -0
  18. package/dist/backend/llvm/alloc.test.d.ts +1 -0
  19. package/dist/backend/llvm/alloc.test.js +102 -0
  20. package/dist/backend/llvm/alloc.test.js.map +1 -0
  21. package/dist/backend/llvm/blocks.d.ts +9 -0
  22. package/dist/backend/llvm/blocks.js +9 -0
  23. package/dist/backend/llvm/blocks.js.map +1 -1
  24. package/dist/backend/llvm/borrowed-receivers.d.ts +7 -0
  25. package/dist/backend/llvm/borrowed-receivers.js +39 -0
  26. package/dist/backend/llvm/borrowed-receivers.js.map +1 -1
  27. package/dist/backend/llvm/call-lifetimes.d.ts +18 -3
  28. package/dist/backend/llvm/call-lifetimes.js +54 -19
  29. package/dist/backend/llvm/call-lifetimes.js.map +1 -1
  30. package/dist/backend/llvm/call-lifetimes.test.js +178 -3
  31. package/dist/backend/llvm/call-lifetimes.test.js.map +1 -1
  32. package/dist/backend/llvm/checked-narrows.d.ts +24 -0
  33. package/dist/backend/llvm/checked-narrows.js +108 -0
  34. package/dist/backend/llvm/checked-narrows.js.map +1 -0
  35. package/dist/backend/llvm/checked-narrows.test.d.ts +1 -0
  36. package/dist/backend/llvm/checked-narrows.test.js +208 -0
  37. package/dist/backend/llvm/checked-narrows.test.js.map +1 -0
  38. package/dist/backend/llvm/classes.d.ts +16 -1
  39. package/dist/backend/llvm/classes.js +60 -49
  40. package/dist/backend/llvm/classes.js.map +1 -1
  41. package/dist/backend/llvm/classes.test.js +68 -6
  42. package/dist/backend/llvm/classes.test.js.map +1 -1
  43. package/dist/backend/llvm/common.d.ts +13 -0
  44. package/dist/backend/llvm/common.js +30 -0
  45. package/dist/backend/llvm/common.js.map +1 -1
  46. package/dist/backend/llvm/constant-callbacks.d.ts +19 -3
  47. package/dist/backend/llvm/constant-callbacks.js +289 -52
  48. package/dist/backend/llvm/constant-callbacks.js.map +1 -1
  49. package/dist/backend/llvm/constant-callbacks.test.js +140 -3
  50. package/dist/backend/llvm/constant-callbacks.test.js.map +1 -1
  51. package/dist/backend/llvm/debug-info.d.ts +6 -2
  52. package/dist/backend/llvm/debug-info.js +11 -1
  53. package/dist/backend/llvm/debug-info.js.map +1 -1
  54. package/dist/backend/llvm/dense-array-access.d.ts +29 -0
  55. package/dist/backend/llvm/dense-array-access.js +335 -0
  56. package/dist/backend/llvm/dense-array-access.js.map +1 -0
  57. package/dist/backend/llvm/dense-array-access.test.d.ts +1 -0
  58. package/dist/backend/llvm/dense-array-access.test.js +238 -0
  59. package/dist/backend/llvm/dense-array-access.test.js.map +1 -0
  60. package/dist/backend/llvm/dyn.d.ts +38 -0
  61. package/dist/backend/llvm/dyn.js +213 -14
  62. package/dist/backend/llvm/dyn.js.map +1 -1
  63. package/dist/backend/llvm/emitter.d.ts +308 -15
  64. package/dist/backend/llvm/emitter.js +1788 -206
  65. package/dist/backend/llvm/emitter.js.map +1 -1
  66. package/dist/backend/llvm/emitter.test.js +89 -7
  67. package/dist/backend/llvm/emitter.test.js.map +1 -1
  68. package/dist/backend/llvm/expr-async.js +62 -8
  69. package/dist/backend/llvm/expr-async.js.map +1 -1
  70. package/dist/backend/llvm/expr-async.test.d.ts +1 -0
  71. package/dist/backend/llvm/expr-async.test.js +62 -0
  72. package/dist/backend/llvm/expr-async.test.js.map +1 -0
  73. package/dist/backend/llvm/expr-bytes.d.ts +1 -1
  74. package/dist/backend/llvm/expr-bytes.js +85 -8
  75. package/dist/backend/llvm/expr-bytes.js.map +1 -1
  76. package/dist/backend/llvm/expr-callbacks.js +29 -21
  77. package/dist/backend/llvm/expr-callbacks.js.map +1 -1
  78. package/dist/backend/llvm/expr-calls.d.ts +5 -0
  79. package/dist/backend/llvm/expr-calls.js +118 -15
  80. package/dist/backend/llvm/expr-calls.js.map +1 -1
  81. package/dist/backend/llvm/expr-containers.d.ts +7 -1
  82. package/dist/backend/llvm/expr-containers.js +246 -32
  83. package/dist/backend/llvm/expr-containers.js.map +1 -1
  84. package/dist/backend/llvm/expr-control.js +113 -8
  85. package/dist/backend/llvm/expr-control.js.map +1 -1
  86. package/dist/backend/llvm/expr-dispatch.js +2 -0
  87. package/dist/backend/llvm/expr-dispatch.js.map +1 -1
  88. package/dist/backend/llvm/expr-dynamic.js +244 -44
  89. package/dist/backend/llvm/expr-dynamic.js.map +1 -1
  90. package/dist/backend/llvm/expr-island.js +3 -8
  91. package/dist/backend/llvm/expr-island.js.map +1 -1
  92. package/dist/backend/llvm/expr-primitives.d.ts +1 -1
  93. package/dist/backend/llvm/expr-primitives.js +93 -36
  94. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  95. package/dist/backend/llvm/expr-primitives.test.js +51 -3
  96. package/dist/backend/llvm/expr-primitives.test.js.map +1 -1
  97. package/dist/backend/llvm/expr-records.js +31 -17
  98. package/dist/backend/llvm/expr-records.js.map +1 -1
  99. package/dist/backend/llvm/expr-stream-bridges.js +96 -29
  100. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  101. package/dist/backend/llvm/expr-stream-bridges.test.js +1 -1
  102. package/dist/backend/llvm/expr-stream-callbacks.js +6 -3
  103. package/dist/backend/llvm/expr-stream-callbacks.js.map +1 -1
  104. package/dist/backend/llvm/integer-remainder.d.ts +11 -0
  105. package/dist/backend/llvm/integer-remainder.js +91 -0
  106. package/dist/backend/llvm/integer-remainder.js.map +1 -1
  107. package/dist/backend/llvm/layouts.d.ts +3 -1
  108. package/dist/backend/llvm/layouts.js +3 -2
  109. package/dist/backend/llvm/layouts.js.map +1 -1
  110. package/dist/backend/llvm/lazy-captures.d.ts +16 -0
  111. package/dist/backend/llvm/lazy-captures.js +96 -0
  112. package/dist/backend/llvm/lazy-captures.js.map +1 -0
  113. package/dist/backend/llvm/lazy-captures.test.d.ts +1 -0
  114. package/dist/backend/llvm/lazy-captures.test.js +78 -0
  115. package/dist/backend/llvm/lazy-captures.test.js.map +1 -0
  116. package/dist/backend/llvm/lib-abi.js +10 -0
  117. package/dist/backend/llvm/lib-abi.js.map +1 -1
  118. package/dist/backend/llvm/lib-filesystem.js +10 -15
  119. package/dist/backend/llvm/lib-filesystem.js.map +1 -1
  120. package/dist/backend/llvm/lib-process.js +8 -8
  121. package/dist/backend/llvm/lib-process.js.map +1 -1
  122. package/dist/backend/llvm/lib-shared.js +1 -0
  123. package/dist/backend/llvm/lib-shared.js.map +1 -1
  124. package/dist/backend/llvm/local-array-reads.d.ts +5 -1
  125. package/dist/backend/llvm/local-array-reads.js +147 -26
  126. package/dist/backend/llvm/local-array-reads.js.map +1 -1
  127. package/dist/backend/llvm/local-array-reads.test.js +85 -4
  128. package/dist/backend/llvm/local-array-reads.test.js.map +1 -1
  129. package/dist/backend/llvm/local-union-storage.d.ts +5 -1
  130. package/dist/backend/llvm/local-union-storage.js +54 -8
  131. package/dist/backend/llvm/local-union-storage.js.map +1 -1
  132. package/dist/backend/llvm/local-union-storage.test.js +94 -6
  133. package/dist/backend/llvm/local-union-storage.test.js.map +1 -1
  134. package/dist/backend/llvm/map-number-lookup.d.ts +18 -0
  135. package/dist/backend/llvm/map-number-lookup.js +62 -0
  136. package/dist/backend/llvm/map-number-lookup.js.map +1 -0
  137. package/dist/backend/llvm/map-read-lifetimes.d.ts +6 -0
  138. package/dist/backend/llvm/map-read-lifetimes.js +24 -3
  139. package/dist/backend/llvm/map-read-lifetimes.js.map +1 -1
  140. package/dist/backend/llvm/map-read-lifetimes.test.js +39 -20
  141. package/dist/backend/llvm/map-read-lifetimes.test.js.map +1 -1
  142. package/dist/backend/llvm/nullable-fields.d.ts +28 -0
  143. package/dist/backend/llvm/nullable-fields.js +45 -0
  144. package/dist/backend/llvm/nullable-fields.js.map +1 -0
  145. package/dist/backend/llvm/nullable-fields.test.d.ts +1 -0
  146. package/dist/backend/llvm/nullable-fields.test.js +48 -0
  147. package/dist/backend/llvm/nullable-fields.test.js.map +1 -0
  148. package/dist/backend/llvm/nullable-unions.d.ts +68 -0
  149. package/dist/backend/llvm/nullable-unions.js +132 -0
  150. package/dist/backend/llvm/nullable-unions.js.map +1 -0
  151. package/dist/backend/llvm/publish.d.ts +58 -0
  152. package/dist/backend/llvm/publish.js +286 -0
  153. package/dist/backend/llvm/publish.js.map +1 -0
  154. package/dist/backend/llvm/rebound-parameters.d.ts +12 -0
  155. package/dist/backend/llvm/rebound-parameters.js +72 -0
  156. package/dist/backend/llvm/rebound-parameters.js.map +1 -0
  157. package/dist/backend/llvm/rebound-parameters.test.d.ts +1 -0
  158. package/dist/backend/llvm/rebound-parameters.test.js +67 -0
  159. package/dist/backend/llvm/rebound-parameters.test.js.map +1 -0
  160. package/dist/backend/llvm/reference-effects.d.ts +18 -2
  161. package/dist/backend/llvm/reference-effects.js +106 -16
  162. package/dist/backend/llvm/reference-effects.js.map +1 -1
  163. package/dist/backend/llvm/reference-effects.test.js +52 -1
  164. package/dist/backend/llvm/reference-effects.test.js.map +1 -1
  165. package/dist/backend/llvm/serialized-ir-emission.test.d.ts +1 -0
  166. package/dist/backend/llvm/serialized-ir-emission.test.js +52 -0
  167. package/dist/backend/llvm/serialized-ir-emission.test.js.map +1 -0
  168. package/dist/backend/llvm/shapes.d.ts +104 -9
  169. package/dist/backend/llvm/shapes.js +356 -46
  170. package/dist/backend/llvm/shapes.js.map +1 -1
  171. package/dist/backend/llvm/shapes.test.js +139 -5
  172. package/dist/backend/llvm/shapes.test.js.map +1 -1
  173. package/dist/backend/llvm/stack-callbacks.js +2 -3
  174. package/dist/backend/llvm/stack-callbacks.js.map +1 -1
  175. package/dist/backend/llvm/stack-captures.d.ts +3 -1
  176. package/dist/backend/llvm/stack-captures.js +5 -3
  177. package/dist/backend/llvm/stack-captures.js.map +1 -1
  178. package/dist/backend/llvm/stack-unions.test.js +43 -3
  179. package/dist/backend/llvm/stack-unions.test.js.map +1 -1
  180. package/dist/backend/llvm/string-construction.d.ts +10 -0
  181. package/dist/backend/llvm/string-construction.js +42 -0
  182. package/dist/backend/llvm/string-construction.js.map +1 -1
  183. package/dist/backend/llvm/string-construction.test.js +1 -1
  184. package/dist/backend/llvm/string-key-hash.d.ts +15 -0
  185. package/dist/backend/llvm/string-key-hash.js +76 -0
  186. package/dist/backend/llvm/string-key-hash.js.map +1 -0
  187. package/dist/backend/llvm/string-key-hash.test.d.ts +1 -0
  188. package/dist/backend/llvm/string-key-hash.test.js +45 -0
  189. package/dist/backend/llvm/string-key-hash.test.js.map +1 -0
  190. package/dist/backend/llvm/string-lifetimes.test.js +65 -15
  191. package/dist/backend/llvm/string-lifetimes.test.js.map +1 -1
  192. package/dist/backend/llvm/string-reads.d.ts +7 -0
  193. package/dist/backend/llvm/string-reads.js +76 -0
  194. package/dist/backend/llvm/string-reads.js.map +1 -0
  195. package/dist/backend/llvm/switch-dispatch.js +1 -1
  196. package/dist/backend/llvm/switch-dispatch.js.map +1 -1
  197. package/dist/backend/llvm/switch-dispatch.test.js +3 -3
  198. package/dist/backend/llvm/switch-dispatch.test.js.map +1 -1
  199. package/dist/backend/llvm/test-boxed-unions.d.ts +5 -0
  200. package/dist/backend/llvm/test-boxed-unions.js +13 -0
  201. package/dist/backend/llvm/test-boxed-unions.js.map +1 -0
  202. package/dist/backend/llvm/union-repr.d.ts +15 -0
  203. package/dist/backend/llvm/union-repr.js +89 -0
  204. package/dist/backend/llvm/union-repr.js.map +1 -0
  205. package/dist/backend/llvm/virtual-borrows.d.ts +31 -0
  206. package/dist/backend/llvm/virtual-borrows.js +82 -0
  207. package/dist/backend/llvm/virtual-borrows.js.map +1 -0
  208. package/dist/backend/llvm/walk-borrows.d.ts +43 -0
  209. package/dist/backend/llvm/walk-borrows.js +150 -0
  210. package/dist/backend/llvm/walk-borrows.js.map +1 -0
  211. package/dist/backend/llvm/walk-borrows.test.d.ts +1 -0
  212. package/dist/backend/llvm/walk-borrows.test.js +119 -0
  213. package/dist/backend/llvm/walk-borrows.test.js.map +1 -0
  214. package/dist/backend/llvm/walkers.d.ts +5 -1
  215. package/dist/backend/llvm/walkers.js +24 -21
  216. package/dist/backend/llvm/walkers.js.map +1 -1
  217. package/dist/backend/may-throw.d.ts +29 -1
  218. package/dist/backend/may-throw.js +349 -3
  219. package/dist/backend/may-throw.js.map +1 -1
  220. package/dist/backend/may-throw.test.js +114 -2
  221. package/dist/backend/may-throw.test.js.map +1 -1
  222. package/dist/backend/native/cache-warm.d.ts +2 -1
  223. package/dist/backend/native/cache-warm.js.map +1 -1
  224. package/dist/backend/native/contracts.d.ts +3 -2
  225. package/dist/backend/native/contracts.js.map +1 -1
  226. package/dist/backend/native/runtime-inputs.d.ts +1 -1
  227. package/dist/backend/native/runtime-inputs.js +2 -0
  228. package/dist/backend/native/runtime-inputs.js.map +1 -1
  229. package/dist/backend/native-codegen-core.d.ts +7 -4
  230. package/dist/backend/native-codegen-core.js +14 -9
  231. package/dist/backend/native-codegen-core.js.map +1 -1
  232. package/dist/backend/native-codegen.d.ts +9 -0
  233. package/dist/backend/native-codegen.js +53 -2
  234. package/dist/backend/native-codegen.js.map +1 -1
  235. package/dist/backend/native-codegen.test.js +64 -10
  236. package/dist/backend/native-codegen.test.js.map +1 -1
  237. package/dist/backend/native-link-info-core.d.ts +3 -2
  238. package/dist/backend/native-link-info-core.js.map +1 -1
  239. package/dist/backend/native-link-info.d.ts +2 -1
  240. package/dist/backend/native-link-info.js.map +1 -1
  241. package/dist/backend/native-link-info.test.js +2 -2
  242. package/dist/backend/native-link-info.test.js.map +1 -1
  243. package/dist/backend/native-tools.d.ts +5 -1
  244. package/dist/backend/native-tools.js +3 -1
  245. package/dist/backend/native-tools.js.map +1 -1
  246. package/dist/backend/optimization.d.ts +25 -0
  247. package/dist/backend/optimization.js +21 -0
  248. package/dist/backend/optimization.js.map +1 -0
  249. package/dist/backend/optimization.test.d.ts +1 -0
  250. package/dist/backend/optimization.test.js +24 -0
  251. package/dist/backend/optimization.test.js.map +1 -0
  252. package/dist/backend/publication.d.ts +28 -0
  253. package/dist/backend/publication.js +155 -0
  254. package/dist/backend/publication.js.map +1 -0
  255. package/dist/backend/publication.test.d.ts +1 -0
  256. package/dist/backend/publication.test.js +72 -0
  257. package/dist/backend/publication.test.js.map +1 -0
  258. package/dist/backend/runtime-abi.d.ts +2 -2
  259. package/dist/backend/runtime-abi.js +2 -2
  260. package/dist/backend/runtime-pack-core.d.ts +15 -4
  261. package/dist/backend/runtime-pack-core.js +23 -12
  262. package/dist/backend/runtime-pack-core.js.map +1 -1
  263. package/dist/backend/runtime-pack-native.d.ts +7 -3
  264. package/dist/backend/runtime-pack-native.js +0 -3
  265. package/dist/backend/runtime-pack-native.js.map +1 -1
  266. package/dist/backend/runtime-pack-targets.test.js +1 -1
  267. package/dist/backend/runtime-pack.d.ts +20 -2
  268. package/dist/backend/runtime-pack.js +23 -0
  269. package/dist/backend/runtime-pack.js.map +1 -1
  270. package/dist/backend/runtime-pack.test.js +115 -2
  271. package/dist/backend/runtime-pack.test.js.map +1 -1
  272. package/dist/backend/target-diagnostics.test.js +1 -1
  273. package/dist/backend/targets.d.ts +2 -1
  274. package/dist/backend/targets.js +1 -1
  275. package/dist/backend/targets.js.map +1 -1
  276. package/dist/cli/command.js +75 -11
  277. package/dist/cli/command.js.map +1 -1
  278. package/dist/cli/host.d.ts +2 -1
  279. package/dist/cli/usage.d.ts +4 -1
  280. package/dist/cli/usage.js +13 -3
  281. package/dist/cli/usage.js.map +1 -1
  282. package/dist/compile-types.d.ts +17 -5
  283. package/dist/coverage/report.d.ts +25 -2
  284. package/dist/coverage/report.js +161 -33
  285. package/dist/coverage/report.js.map +1 -1
  286. package/dist/coverage/verdict-cache.d.ts +63 -0
  287. package/dist/coverage/verdict-cache.js +193 -0
  288. package/dist/coverage/verdict-cache.js.map +1 -0
  289. package/dist/coverage/verdict-cache.test.d.ts +1 -0
  290. package/dist/coverage/verdict-cache.test.js +52 -0
  291. package/dist/coverage/verdict-cache.test.js.map +1 -0
  292. package/dist/coverage/verdict-semantic.d.ts +2 -0
  293. package/dist/coverage/verdict-semantic.js +24 -0
  294. package/dist/coverage/verdict-semantic.js.map +1 -0
  295. package/dist/coverage/verdict-startup.d.ts +15 -0
  296. package/dist/coverage/verdict-startup.js +66 -0
  297. package/dist/coverage/verdict-startup.js.map +1 -0
  298. package/dist/diagnostics/diagnostic.d.ts +29 -1
  299. package/dist/diagnostics/diagnostic.js +84 -12
  300. package/dist/diagnostics/diagnostic.js.map +1 -1
  301. package/dist/diagnostics/envelope.d.ts +130 -0
  302. package/dist/diagnostics/envelope.js +257 -0
  303. package/dist/diagnostics/envelope.js.map +1 -0
  304. package/dist/diagnostics/envelope.test.d.ts +1 -0
  305. package/dist/diagnostics/envelope.test.js +91 -0
  306. package/dist/diagnostics/envelope.test.js.map +1 -0
  307. package/dist/diagnostics/render.d.ts +3 -0
  308. package/dist/diagnostics/render.js +13 -2
  309. package/dist/diagnostics/render.js.map +1 -1
  310. package/dist/executable/executable-cache.d.ts +2 -2
  311. package/dist/executable/executable-cache.js +7 -4
  312. package/dist/executable/executable-cache.js.map +1 -1
  313. package/dist/executable/executable-cache.test.js +20 -0
  314. package/dist/executable/executable-cache.test.js.map +1 -1
  315. package/dist/executable/prepare.d.ts +4 -0
  316. package/dist/executable/prepare.js +7 -1
  317. package/dist/executable/prepare.js.map +1 -1
  318. package/dist/frontend/analysis.js +1 -0
  319. package/dist/frontend/analysis.js.map +1 -1
  320. package/dist/frontend/lowering/abstract-equality.js +5 -49
  321. package/dist/frontend/lowering/abstract-equality.js.map +1 -1
  322. package/dist/frontend/lowering/array-values.d.ts +30 -0
  323. package/dist/frontend/lowering/array-values.js +246 -1
  324. package/dist/frontend/lowering/array-values.js.map +1 -1
  325. package/dist/frontend/lowering/builtins/errors.js +17 -2
  326. package/dist/frontend/lowering/builtins/errors.js.map +1 -1
  327. package/dist/frontend/lowering/builtins/numbers.js +29 -1
  328. package/dist/frontend/lowering/builtins/numbers.js.map +1 -1
  329. package/dist/frontend/lowering/builtins/process.d.ts +10 -17
  330. package/dist/frontend/lowering/builtins/process.js +66 -29
  331. package/dist/frontend/lowering/builtins/process.js.map +1 -1
  332. package/dist/frontend/lowering/builtins/workers.js +4 -2
  333. package/dist/frontend/lowering/builtins/workers.js.map +1 -1
  334. package/dist/frontend/lowering/caught-value-dispatch.d.ts +11 -0
  335. package/dist/frontend/lowering/caught-value-dispatch.js +87 -0
  336. package/dist/frontend/lowering/caught-value-dispatch.js.map +1 -0
  337. package/dist/frontend/lowering/class-assertions.d.ts +12 -0
  338. package/dist/frontend/lowering/class-assertions.js +229 -5
  339. package/dist/frontend/lowering/class-assertions.js.map +1 -1
  340. package/dist/frontend/lowering/class-callbacks.d.ts +3 -3
  341. package/dist/frontend/lowering/class-callbacks.js +18 -5
  342. package/dist/frontend/lowering/class-callbacks.js.map +1 -1
  343. package/dist/frontend/lowering/class-dynamic-dispatch.d.ts +1 -0
  344. package/dist/frontend/lowering/class-dynamic-dispatch.js +12 -6
  345. package/dist/frontend/lowering/class-dynamic-dispatch.js.map +1 -1
  346. package/dist/frontend/lowering/class-method-mutations.d.ts +5 -0
  347. package/dist/frontend/lowering/class-method-mutations.js +472 -0
  348. package/dist/frontend/lowering/class-method-mutations.js.map +1 -0
  349. package/dist/frontend/lowering/class-method-values.js +25 -5
  350. package/dist/frontend/lowering/class-method-values.js.map +1 -1
  351. package/dist/frontend/lowering/class-prototypes.js +8 -2
  352. package/dist/frontend/lowering/class-prototypes.js.map +1 -1
  353. package/dist/frontend/lowering/class-unions.d.ts +16 -0
  354. package/dist/frontend/lowering/class-unions.js +127 -0
  355. package/dist/frontend/lowering/class-unions.js.map +1 -0
  356. package/dist/frontend/lowering/coercions/builders.js +8 -1
  357. package/dist/frontend/lowering/coercions/builders.js.map +1 -1
  358. package/dist/frontend/lowering/coercions/function-adapters.js +2 -0
  359. package/dist/frontend/lowering/coercions/function-adapters.js.map +1 -1
  360. package/dist/frontend/lowering/coercions/identity.d.ts +7 -0
  361. package/dist/frontend/lowering/coercions/identity.js +26 -0
  362. package/dist/frontend/lowering/coercions/identity.js.map +1 -0
  363. package/dist/frontend/lowering/coercions/state.d.ts +6 -0
  364. package/dist/frontend/lowering/coercions/state.js +2 -0
  365. package/dist/frontend/lowering/coercions/state.js.map +1 -1
  366. package/dist/frontend/lowering/coercions/structural.js +9 -6
  367. package/dist/frontend/lowering/coercions/structural.js.map +1 -1
  368. package/dist/frontend/lowering/coercions/unions.d.ts +8 -0
  369. package/dist/frontend/lowering/coercions/unions.js +67 -2
  370. package/dist/frontend/lowering/coercions/unions.js.map +1 -1
  371. package/dist/frontend/lowering/collation-repertoire.d.ts +1 -0
  372. package/dist/frontend/lowering/collation-repertoire.js +19 -0
  373. package/dist/frontend/lowering/collation-repertoire.js.map +1 -0
  374. package/dist/frontend/lowering/collection-narrowing.d.ts +6 -0
  375. package/dist/frontend/lowering/collection-narrowing.js +31 -0
  376. package/dist/frontend/lowering/collection-narrowing.js.map +1 -0
  377. package/dist/frontend/lowering/construction-escapes.d.ts +38 -0
  378. package/dist/frontend/lowering/construction-escapes.js +146 -0
  379. package/dist/frontend/lowering/construction-escapes.js.map +1 -0
  380. package/dist/frontend/lowering/construction-escapes.test.d.ts +1 -0
  381. package/dist/frontend/lowering/construction-escapes.test.js +150 -0
  382. package/dist/frontend/lowering/construction-escapes.test.js.map +1 -0
  383. package/dist/frontend/lowering/containers/array-callback-call.d.ts +7 -0
  384. package/dist/frontend/lowering/containers/array-callback-call.js +52 -0
  385. package/dist/frontend/lowering/containers/array-callback-call.js.map +1 -0
  386. package/dist/frontend/lowering/containers/array-callbacks.d.ts +1 -1
  387. package/dist/frontend/lowering/containers/array-callbacks.js +416 -65
  388. package/dist/frontend/lowering/containers/array-callbacks.js.map +1 -1
  389. package/dist/frontend/lowering/containers/array-concat.d.ts +7 -0
  390. package/dist/frontend/lowering/containers/array-concat.js +130 -0
  391. package/dist/frontend/lowering/containers/array-concat.js.map +1 -0
  392. package/dist/frontend/lowering/containers/array-construction.d.ts +2 -0
  393. package/dist/frontend/lowering/containers/array-construction.js +110 -5
  394. package/dist/frontend/lowering/containers/array-construction.js.map +1 -1
  395. package/dist/frontend/lowering/containers/array-methods.d.ts +1 -11
  396. package/dist/frontend/lowering/containers/array-methods.js +274 -144
  397. package/dist/frontend/lowering/containers/array-methods.js.map +1 -1
  398. package/dist/frontend/lowering/containers/callback-arguments.js +43 -1
  399. package/dist/frontend/lowering/containers/callback-arguments.js.map +1 -1
  400. package/dist/frontend/lowering/containers/collection-methods.js +10 -1
  401. package/dist/frontend/lowering/containers/collection-methods.js.map +1 -1
  402. package/dist/frontend/lowering/containers/indexed-objects.js +56 -70
  403. package/dist/frontend/lowering/containers/indexed-objects.js.map +1 -1
  404. package/dist/frontend/lowering/containers/string-and-regexp.js +7 -63
  405. package/dist/frontend/lowering/containers/string-and-regexp.js.map +1 -1
  406. package/dist/frontend/lowering/cycle-initialization.d.ts +46 -0
  407. package/dist/frontend/lowering/cycle-initialization.js +303 -0
  408. package/dist/frontend/lowering/cycle-initialization.js.map +1 -0
  409. package/dist/frontend/lowering/divergences.d.ts +9 -0
  410. package/dist/frontend/lowering/divergences.js +630 -0
  411. package/dist/frontend/lowering/divergences.js.map +1 -0
  412. package/dist/frontend/lowering/expressions/evaluation-safety.js +4 -0
  413. package/dist/frontend/lowering/expressions/evaluation-safety.js.map +1 -1
  414. package/dist/frontend/lowering/expressions/object-literals.js +178 -34
  415. package/dist/frontend/lowering/expressions/object-literals.js.map +1 -1
  416. package/dist/frontend/lowering/expressions/union-field-write.d.ts +4 -4
  417. package/dist/frontend/lowering/expressions/union-field-write.js +54 -44
  418. package/dist/frontend/lowering/expressions/union-field-write.js.map +1 -1
  419. package/dist/frontend/lowering/iterator-adapters.d.ts +20 -0
  420. package/dist/frontend/lowering/iterator-adapters.js +605 -0
  421. package/dist/frontend/lowering/iterator-adapters.js.map +1 -0
  422. package/dist/frontend/lowering/lower-array-sort.d.ts +1 -1
  423. package/dist/frontend/lowering/lower-array-sort.js +44 -5
  424. package/dist/frontend/lowering/lower-array-sort.js.map +1 -1
  425. package/dist/frontend/lowering/lower-array-sort.test.d.ts +1 -0
  426. package/dist/frontend/lowering/lower-array-sort.test.js +31 -0
  427. package/dist/frontend/lowering/lower-array-sort.test.js.map +1 -0
  428. package/dist/frontend/lowering/lower-assert.js +7 -0
  429. package/dist/frontend/lowering/lower-assert.js.map +1 -1
  430. package/dist/frontend/lowering/lower-builtin-values.js +1 -0
  431. package/dist/frontend/lowering/lower-builtin-values.js.map +1 -1
  432. package/dist/frontend/lowering/lower-calls.d.ts +8 -3
  433. package/dist/frontend/lowering/lower-calls.js +388 -278
  434. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  435. package/dist/frontend/lowering/lower-classes.d.ts +6 -0
  436. package/dist/frontend/lowering/lower-classes.js +117 -65
  437. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  438. package/dist/frontend/lowering/lower-exprs.d.ts +22 -2
  439. package/dist/frontend/lowering/lower-exprs.js +811 -254
  440. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  441. package/dist/frontend/lowering/lower-functions.js +32 -19
  442. package/dist/frontend/lowering/lower-functions.js.map +1 -1
  443. package/dist/frontend/lowering/lower-generators.d.ts +19 -13
  444. package/dist/frontend/lowering/lower-generators.js +67 -44
  445. package/dist/frontend/lowering/lower-generators.js.map +1 -1
  446. package/dist/frontend/lowering/lower-inspect.js +29 -4
  447. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  448. package/dist/frontend/lowering/lower-island.d.ts +1 -1
  449. package/dist/frontend/lowering/lower-island.js +24 -7
  450. package/dist/frontend/lowering/lower-island.js.map +1 -1
  451. package/dist/frontend/lowering/lower-modules.js +100 -3
  452. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  453. package/dist/frontend/lowering/lower-stmts.js +82 -64
  454. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  455. package/dist/frontend/lowering/lowerer.d.ts +115 -2
  456. package/dist/frontend/lowering/lowerer.js +1266 -88
  457. package/dist/frontend/lowering/lowerer.js.map +1 -1
  458. package/dist/frontend/lowering/native-ffi.js +3 -1
  459. package/dist/frontend/lowering/native-ffi.js.map +1 -1
  460. package/dist/frontend/lowering/runtime-optional-bounds.d.ts +19 -0
  461. package/dist/frontend/lowering/runtime-optional-bounds.js +354 -0
  462. package/dist/frontend/lowering/runtime-optional-bounds.js.map +1 -0
  463. package/dist/frontend/lowering/runtime-optional-bounds.test.d.ts +1 -0
  464. package/dist/frontend/lowering/runtime-optional-bounds.test.js +66 -0
  465. package/dist/frontend/lowering/runtime-optional-bounds.test.js.map +1 -0
  466. package/dist/frontend/lowering/runtime-optional-elements.d.ts +117 -0
  467. package/dist/frontend/lowering/runtime-optional-elements.js +738 -0
  468. package/dist/frontend/lowering/runtime-optional-elements.js.map +1 -0
  469. package/dist/frontend/lowering/runtime-optional-guards.d.ts +36 -0
  470. package/dist/frontend/lowering/runtime-optional-guards.js +375 -0
  471. package/dist/frontend/lowering/runtime-optional-guards.js.map +1 -0
  472. package/dist/frontend/lowering/runtime-optional-guards.test.d.ts +1 -0
  473. package/dist/frontend/lowering/runtime-optional-guards.test.js +92 -0
  474. package/dist/frontend/lowering/runtime-optional-guards.test.js.map +1 -0
  475. package/dist/frontend/lowering/runtime-optional-ownership.d.ts +63 -0
  476. package/dist/frontend/lowering/runtime-optional-ownership.js +382 -0
  477. package/dist/frontend/lowering/runtime-optional-ownership.js.map +1 -0
  478. package/dist/frontend/lowering/runtime-optional-provenance.d.ts +16 -0
  479. package/dist/frontend/lowering/runtime-optional-provenance.js +41 -0
  480. package/dist/frontend/lowering/runtime-optional-provenance.js.map +1 -0
  481. package/dist/frontend/lowering/sanitize-class-types.test.js +2 -2
  482. package/dist/frontend/lowering/strict-equality.d.ts +9 -0
  483. package/dist/frontend/lowering/strict-equality.js +116 -0
  484. package/dist/frontend/lowering/strict-equality.js.map +1 -0
  485. package/dist/frontend/lowering/string-index-bounds.d.ts +36 -0
  486. package/dist/frontend/lowering/string-index-bounds.js +537 -0
  487. package/dist/frontend/lowering/string-index-bounds.js.map +1 -0
  488. package/dist/frontend/lowering/string-index-bounds.test.d.ts +1 -0
  489. package/dist/frontend/lowering/string-index-bounds.test.js +223 -0
  490. package/dist/frontend/lowering/string-index-bounds.test.js.map +1 -0
  491. package/dist/frontend/lowering/surfaces.js +3 -0
  492. package/dist/frontend/lowering/surfaces.js.map +1 -1
  493. package/dist/frontend/lowering/union-comparison.d.ts +5 -0
  494. package/dist/frontend/lowering/union-comparison.js +47 -0
  495. package/dist/frontend/lowering/union-comparison.js.map +1 -0
  496. package/dist/frontend/lowering/union-narrow.d.ts +2 -0
  497. package/dist/frontend/lowering/union-narrow.js +10 -6
  498. package/dist/frontend/lowering/union-narrow.js.map +1 -1
  499. package/dist/frontend/program-analysis.d.ts +10 -0
  500. package/dist/frontend/program-analysis.js +7 -0
  501. package/dist/frontend/program-analysis.js.map +1 -1
  502. package/dist/frontend/program.d.ts +63 -2
  503. package/dist/frontend/program.js +1038 -61
  504. package/dist/frontend/program.js.map +1 -1
  505. package/dist/frontend/program.test.js +122 -6
  506. package/dist/frontend/program.test.js.map +1 -1
  507. package/dist/frontend/resolve.d.ts +6 -0
  508. package/dist/frontend/resolve.js +14 -0
  509. package/dist/frontend/resolve.js.map +1 -1
  510. package/dist/frontend/threads-import.d.ts +5 -0
  511. package/dist/frontend/threads-import.js +21 -0
  512. package/dist/frontend/threads-import.js.map +1 -0
  513. package/dist/frontend/type-mapper.d.ts +4 -1
  514. package/dist/frontend/type-mapper.js +161 -20
  515. package/dist/frontend/type-mapper.js.map +1 -1
  516. package/dist/frontend/type-mapper.test.js +15 -3
  517. package/dist/frontend/type-mapper.test.js.map +1 -1
  518. package/dist/frontend/worker-target.d.ts +4 -0
  519. package/dist/frontend/worker-target.js +8 -1
  520. package/dist/frontend/worker-target.js.map +1 -1
  521. package/dist/index.d.ts +6 -1
  522. package/dist/index.js +70 -16
  523. package/dist/index.js.map +1 -1
  524. package/dist/ir/analysis.d.ts +3 -1
  525. package/dist/ir/analysis.js +4 -0
  526. package/dist/ir/analysis.js.map +1 -1
  527. package/dist/ir/build.d.ts +5 -0
  528. package/dist/ir/build.js +7 -0
  529. package/dist/ir/build.js.map +1 -1
  530. package/dist/ir/builtin-signatures.d.ts +1 -1
  531. package/dist/ir/builtin-signatures.js +5 -3
  532. package/dist/ir/builtin-signatures.js.map +1 -1
  533. package/dist/ir/constant-tables.test.js +1 -1
  534. package/dist/ir/initializer-bindings.test.js +1 -1
  535. package/dist/ir/int32-slots.d.ts +72 -0
  536. package/dist/ir/int32-slots.js +837 -0
  537. package/dist/ir/int32-slots.js.map +1 -0
  538. package/dist/ir/int32-slots.test.d.ts +1 -0
  539. package/dist/ir/int32-slots.test.js +222 -0
  540. package/dist/ir/int32-slots.test.js.map +1 -0
  541. package/dist/ir/integer-ranges.d.ts +19 -3
  542. package/dist/ir/integer-ranges.js +168 -23
  543. package/dist/ir/integer-ranges.js.map +1 -1
  544. package/dist/ir/integer-ranges.test.js +74 -2
  545. package/dist/ir/integer-ranges.test.js.map +1 -1
  546. package/dist/ir/ir.d.ts +101 -20
  547. package/dist/ir/ir.js +20 -6
  548. package/dist/ir/ir.js.map +1 -1
  549. package/dist/ir/scalar-records.test.js +1 -1
  550. package/dist/ir/serialize.d.ts +1 -1
  551. package/dist/ir/serialize.js +3 -3
  552. package/dist/ir/serialize.js.map +1 -1
  553. package/dist/ir/traverse.js +8 -0
  554. package/dist/ir/traverse.js.map +1 -1
  555. package/dist/ir/validate.js +139 -22
  556. package/dist/ir/validate.js.map +1 -1
  557. package/dist/ir/validate.test.js +308 -8
  558. package/dist/ir/validate.test.js.map +1 -1
  559. package/dist/library/fence-eval.test.js +1 -1
  560. package/dist/library/int-infer.test.js +4 -4
  561. package/dist/library/library-cache.test.js +5 -5
  562. package/dist/library/library-profile.d.ts +2 -1
  563. package/dist/library/library-profile.js +4 -3
  564. package/dist/library/library-profile.js.map +1 -1
  565. package/dist/native/cache.d.ts +3 -0
  566. package/dist/native/cache.js +49 -1
  567. package/dist/native/cache.js.map +1 -1
  568. package/dist/native/cache.test.js +21 -1
  569. package/dist/native/cache.test.js.map +1 -1
  570. package/dist/native/cli.js +1 -1
  571. package/dist/native/cli.js.map +1 -1
  572. package/dist/native/compiler.d.ts +5 -0
  573. package/dist/native/compiler.js +51 -4
  574. package/dist/native/compiler.js.map +1 -1
  575. package/dist/native/compiler.test.js +2 -2
  576. package/dist/native/library.d.ts +2 -1
  577. package/dist/native/library.js.map +1 -1
  578. package/dist/native/prepare.d.ts +4 -0
  579. package/dist/native/prepare.js +28 -3
  580. package/dist/native/prepare.js.map +1 -1
  581. package/dist/native/sanitizer.d.ts +3 -2
  582. package/dist/native/sanitizer.js +3 -13
  583. package/dist/native/sanitizer.js.map +1 -1
  584. package/dist/startup-cache.d.ts +2 -0
  585. package/dist/startup-cache.js +2 -0
  586. package/dist/startup-cache.js.map +1 -1
  587. package/package.json +20 -20
  588. package/surface-manifest.json +8 -2
@@ -1,4 +1,5 @@
1
1
  import { emitLlvmLayouts } from "./layouts.js";
2
+ import { inlineAllocSupported } from "./alloc.js";
2
3
  import { llvmBytes as llBytes } from "../literals.js";
3
4
  import { InternalCompilerError } from "../../errors.js";
4
5
  /** Lower typed IR to LLVM, using the runtime's C ABI.
@@ -18,10 +19,11 @@ import { InternalCompilerError } from "../../errors.js";
18
19
  * Dynamic values and islands share the runtime ABI; embedded npm modules use
19
20
  * compressed source tables that the runtime inflates on demand.
20
21
  */
22
+ import { createHash } from "node:crypto";
21
23
  import { deflateRawSync } from "node:zlib";
22
- import { endsWithJump, matchStringSelfConcat, streamTypedRefEligible } from "../../ir/analysis.js";
24
+ import { endsWithJump, matchStringSelfConcat, streamTypedRefEligible, undefinedArmTag, } from "../../ir/analysis.js";
23
25
  import { emitLibraryIdentityLines } from "../library-identity-markers.js";
24
- import { CAUGHT, ffiCallbackType, isDynTypedRefType, isFfiContextParam, isRefCounted, isUnitType, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeKey, STRING, VOID, } from "../../ir/ir.js";
26
+ import { CAUGHT, ffiCallbackType, isDynTypedRefType, isFfiContextParam, isRefCounted, isUnitType, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeEquals, typeKey, STRING, VOID, } from "../../ir/ir.js";
25
27
  import { moduleRuntimeFeatures, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, NPM_COMPRESS_MIN, } from "../../ir/runtime-features.js";
26
28
  import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, matchIntegerCountedForLoop, } from "../../ir/integer-loops.js";
27
29
  import { emitCountedLoopLimit } from "./counted-loops.js";
@@ -31,8 +33,9 @@ import { emitByteWindowGuard, findInitializedByteLoopBindings, matchByteWindow,
31
33
  import { canDeferSplitPiece, emitSplitSpanNext, materializeSplitPiece, emitSplitCursor, emitSplitNext, emitSplitScratch, emitSplitSnapshot, findPrivateSplitLocals, loadSplitSnapshot, storeSplitSnapshot, stringSplit, } from "./split-loops.js";
32
34
  import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
33
35
  import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
34
- import { everyStmtList } from "../../ir/traverse.js";
35
- import { analyzeIntegerRanges } from "../../ir/integer-ranges.js";
36
+ import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
37
+ import { analyzeIntegerRanges, INT32_RANGE } from "../../ir/integer-ranges.js";
38
+ import { analyzeInt32Slots } from "../../ir/int32-slots.js";
36
39
  import { findIntegerViews } from "./integer-views.js";
37
40
  import { findInitializerBindings, withInitializerBindings } from "../../ir/initializer-bindings.js";
38
41
  import { findConstantNumericTables } from "../../ir/constant-tables.js";
@@ -44,29 +47,37 @@ import { analyzeCallLifetimes } from "./call-lifetimes.js";
44
47
  import { canStackUnion, emitStackUnion, findLocalStackUnions } from "./stack-unions.js";
45
48
  import { allocateLocalUnion, findLocalUnionStorage, storeLocalUnion, } from "./local-union-storage.js";
46
49
  import { BlockBuilder } from "./blocks.js";
47
- import { emitLocalArrayRead, emitBorrowedArrayRead, findLocalArrayReads, findCallArrayReads, OptionalArrayReads, } from "./local-array-reads.js";
50
+ import { NullableRefFields } from "./nullable-fields.js";
51
+ import { emitLocalArrayRead, emitProjectedArrayRead, emitBorrowedArrayRead, findLocalArrayReads, findCallArrayReads, OptionalArrayReads, } from "./local-array-reads.js";
52
+ import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js";
53
+ import { emitDenseArrayGet, emitDenseArraySet } from "./dense-array-access.js";
48
54
  import { emitStackMapRead, findMapReadLifetimes, matchMapRead, } from "./map-read-lifetimes.js";
49
- import { emitBorrowedFieldSequence } from "./borrowed-receivers.js";
55
+ import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
50
56
  import { emitBorrowedInput } from "./borrowed-inputs.js";
57
+ import { analyzeWalks, findWalkBorrows } from "./walk-borrows.js";
58
+ import { findReboundParameters } from "./rebound-parameters.js";
51
59
  import { ReferenceEffects } from "./reference-effects.js";
52
60
  import { LlvmDebugInfo } from "./debug-info.js";
53
61
  import { StackCallbacks } from "./stack-callbacks.js";
54
62
  import { findLoopArrayBorrows } from "./loop-array-borrows.js";
55
- import { findConstantCallbacks } from "./constant-callbacks.js";
63
+ import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
56
64
  import { findScalarStringSlices, emitStringSliceSnapshot, } from "./string-slices.js";
65
+ import { LazyCaptures } from "./lazy-captures.js";
57
66
  import { StackCaptures } from "./stack-captures.js";
58
- import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText, } from "./common.js";
67
+ import { ABSENT_FIELD_PAYLOAD, emitFieldAbsentTest, f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText, } from "./common.js";
59
68
  import { ffiExtendsNarrowIntegers } from "../targets.js";
60
69
  import { emitLiteralExpr, emitOperatorExpr, emitStringExpr, emitContainerExpr, emitRecordExpr, } from "./expr-primitives.js";
61
70
  import { emitControlExpr } from "./expr-control.js";
62
- import { emitCallExpr } from "./expr-calls.js";
71
+ import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
63
72
  import { emitDynamicExpr } from "./expr-dynamic.js";
64
73
  import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js";
74
+ import { PublishWalkers } from "./publish.js";
75
+ import { computePublishedTypes } from "../publication.js";
65
76
  import { emitJsInteropExpr, emitExpr } from "./expr-dispatch.js";
66
77
  import { emitJsMarshal, emitJsOp, emitJsExit, islandAdapter, islandTypedAdapter, } from "./expr-island.js";
67
78
  import { dynKind, raceAdapterFor, genResultThunkFor, childExitThunkFor, childExitSignalThunkFor, childDataThunkFor, execFileThunkFor, ipcMessageThunkFor, ipcSendThunkFor, emitterFixedAdapter, wrapEmitterListener, unwrapNullableClosure, closeBindThunkFor, closeOverrideWrapFor, } from "./expr-callbacks.js";
68
79
  import { streamDataAdapter, streamDoneFnFor, cryptoBytesThunkFor, fsRenameThunkFor, streamCbThunkFor, zlibBytesThunkFor, } from "./expr-stream-callbacks.js";
69
- import { emitStableReceiver, resolveThunkFor, tagInSet, arrPush, emitArrayCopyLoop, emitStrIntrinsic, emitArrIntrinsic, wrapNullable, emitMapNew, mapSet, emitMapLikeIntrinsic, emitSetNew, } from "./expr-containers.js";
80
+ import { emitStableReceiver, resolveThunkFor, tagInSet, arrPush, emitArrayCopyLoop, emitStrIntrinsic, emitArrIntrinsic, emitDiscardedSplice, wrapNullable, emitMapNew, mapSet, emitMapLikeIntrinsic, emitSetNew, } from "./expr-containers.js";
70
81
  import { emitIntegerLoopIndex, emitBytesIndex, emitBytesData, emitBytesLength, emitBytesGet, emitToUint32, emitBytesSet, emitBytesIntrinsic, } from "./expr-bytes.js";
71
82
  import { emitRegexIntrinsic, emitRecordKeyGet, keyedRecordReadInto } from "./expr-records.js";
72
83
  import { dynPromiseAdapter, streamTypedRefCommitAdapter, liveDynUnionRefAdapter, streamTypedRefBoxValue, streamTypedRefMaterializeAdapter, streamFromArrayAdapter, } from "./expr-stream-bridges.js";
@@ -77,9 +88,16 @@ import { emitNetworkHttpLibCall } from "./lib-network.js";
77
88
  import { emitAssertInspectLibCall, emitIoLibCall, emitGenericLibCall, emitLibCall, } from "./lib-dispatch.js";
78
89
  import { buildClassGraph, classMembershipIntervals, emitClassMembershipHelper, classEnvironmentIndex, classFieldIndex, classStructSym, } from "./classes.js";
79
90
  import { LlDyn } from "./dyn.js";
91
+ import { VirtualBorrows } from "./virtual-borrows.js";
80
92
  import { LlvmUnsupportedError } from "./unsupported.js";
81
93
  import { LlWalkers } from "./walkers.js";
82
- import { arrNewCall, boxAccess, boxNewCall, computeTraced, elemAccess, FN_ATTRS, llFieldType, releaseSym, retainSym, traceArg, typedRefConstructor, vAdapters, } from "./shapes.js";
94
+ import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js";
95
+ import { emitNullablePresent, emitUnionPeek, emitUnionTag } from "./union-repr.js";
96
+ import { literalCapWord, literalCapWord32 } from "./string-key-hash.js";
97
+ import { NUMBER_MAP_ENTRY_DECL } from "./map-number-lookup.js";
98
+ import { STRING_READ_HELPERS } from "./string-reads.js";
99
+ import { emitConcatInputs, emitMixedConcat, numberPart, stringParts, } from "./string-construction.js";
100
+ import { arrNewCall, boxAccess, boxNewCall, boxReleaseSym, computeTraced, elemAccess, emitInlineRcHelpers, emitNullableRetainWrappers, FN_ATTRS, inlineRcDecls, llFieldType, releaseSym, retainSym, traceArg, typedRefConstructor, vAdapters, } from "./shapes.js";
83
101
  export { LlvmUnsupportedError } from "./unsupported.js";
84
102
  function ffiCallbackDummyLl(callback) {
85
103
  switch (callback.returns) {
@@ -125,6 +143,41 @@ export function emitLlvmModuleSource(mod, options = {}) {
125
143
  function llStrBytes(text) {
126
144
  return llBytes(Buffer.from(text, "utf8"));
127
145
  }
146
+ const NO_BORROWED = new Set();
147
+ /** Whether any node of `e` names the local (reads, writes, declarations,
148
+ * captures). */
149
+ function mentionsLocal(e, localId) {
150
+ const exprNames = (node) => {
151
+ if (node.kind === "varRef" || node.kind === "incDec" || node.kind === "assignExpr")
152
+ return node.localId === localId;
153
+ if (node.kind === "closure")
154
+ return node.captures.includes(localId);
155
+ if (node.kind === "classRef")
156
+ return node.captures?.includes(localId) === true;
157
+ return false;
158
+ };
159
+ const stmtNames = (node) => {
160
+ if (node.kind === "varDecl" ||
161
+ node.kind === "assign" ||
162
+ node.kind === "forOf" ||
163
+ node.kind === "rethrow")
164
+ return node.localId === localId;
165
+ return false;
166
+ };
167
+ let found = false;
168
+ const expr = (node) => {
169
+ if (found || exprNames(node))
170
+ return !(found = true);
171
+ return everyExprChild(node, expr, stmt);
172
+ };
173
+ const stmt = (node) => {
174
+ if (found || stmtNames(node))
175
+ return !(found = true);
176
+ return everyStmtChild(node, expr, stmt);
177
+ };
178
+ expr(e);
179
+ return found;
180
+ }
128
181
  export class LlEmitter {
129
182
  mod;
130
183
  localArrayReads = new Map();
@@ -173,15 +226,23 @@ export class LlEmitter {
173
226
  ffiExtendNarrowIntegers;
174
227
  cycleColorOffset;
175
228
  wasi;
229
+ /** ELF worker executables give thread-locals the executable TLS models. */
230
+ executableTls;
176
231
  emitLibraryIdentity;
177
232
  runtimeAbiMarker;
178
233
  /** Interned string literals: UTF-8 text → { symbol, byte length } —
179
234
  * first-use order, the runtime ABI’s determinism discipline. */
180
235
  literals = new Map();
236
+ /** Symbols of interned constant data, named by content (contentSymbol). */
237
+ contentSymbols = new Set();
181
238
  /** Interned unit-armed union instances: "unionId:tag" → symbol — one
182
239
  * immortal (rc == SIZE_MAX) static per (union, unit tag).
183
240
  * RC entry points and the collector skip immortals. */
184
241
  unitInstances = new Map();
242
+ /** unionId → symbol of the union's immortal ABSENT field-slot instance. */
243
+ absentInstances = new Map();
244
+ /** The nullable-union ABSENT sentinel was referenced (absentInstanceRef). */
245
+ needsNullableAbsent = false;
185
246
  immortalValues = new Set();
186
247
  /** Regex literal templates: "<flags>/<pattern>" → { symbol, interned
187
248
  * source/flags literal refs } — one immortal ScrRegex template per distinct
@@ -215,18 +276,45 @@ export class LlEmitter {
215
276
  needsBadTag = false;
216
277
  needsBadKey = false;
217
278
  needsRetainBox = false;
279
+ /** Inline RC fast-path helpers requested by call sites (shapes.ts); null
280
+ * when inline RC is off. */
281
+ rcHelpers;
218
282
  fnByName = new Map();
283
+ /** Emitted LLVM text of every IR function body, by function name: the
284
+ * fiberless-async analysis scans it for suspension entry points. */
285
+ fnDefText = new Map();
219
286
  referenceEffects;
220
287
  optionalArrayReads;
288
+ checkedNarrows;
221
289
  callArrayReads = new Map();
222
290
  mapReadLifetimes = { locals: new Map(), arguments: new Map() };
223
291
  callLifetimes;
224
292
  constantCallbacks;
225
293
  currentConstantCallbacks = new Map();
226
294
  stackCallbacks;
295
+ receiverReaders;
227
296
  stackCaptures;
297
+ lazyCaptures;
298
+ /** Unchanged captured parameters of the current function: each maps to
299
+ * the slot caching its box, NULL until the first closure needs it. */
300
+ lazyCaptureBoxes = new Map();
301
+ lazyStackBoxes = new Map();
228
302
  borrowedParameters = new Set();
303
+ /** Parameters proven projection-only: callers may pass stack boxes. */
304
+ projectedParameters = new Set();
229
305
  stableCallBindings = new Set();
306
+ /** Locals and rebound parameters holding borrowed pointers for their
307
+ * whole lifetime (walk-borrows.ts), per function and for the current one. */
308
+ walkBorrowsByFunction = new Map();
309
+ walkBorrows = new Set();
310
+ /** Functions whose borrowing body returns its result without a reference
311
+ * (analyzeBorrowedReturns), and whether the current body is one. */
312
+ borrowedReturns = new Set();
313
+ /** Rebound parameters that keep the borrowed convention with an owner
314
+ * slot (rebound-parameters.ts), per function, and the current owner slots. */
315
+ reboundByFunction = new Map();
316
+ reboundOwnerSlots = new Map();
317
+ currentBorrowedReturn = false;
230
318
  /** Manifest-bound native imports, used by ffiCall emission. */
231
319
  ffiByName = new Map();
232
320
  /** C-ABI callback trampolines and (for raw/no-userdata callbacks) their
@@ -245,6 +333,15 @@ export class LlEmitter {
245
333
  * checks are emitted only after calls that can actually raise. */
246
334
  mayThrow;
247
335
  indirectMayThrow;
336
+ /** Worker programs: the functions that poll for termination on entry
337
+ * (workerTerminationPolls); null outside worker programs. */
338
+ workerEntryPolls;
339
+ /** Functions that check the native stack guard on entry
340
+ * (stackCheckedFunctions); null where the runtime has no guard. */
341
+ stackChecks;
342
+ /** Static types whose values can be published (@scriptc/threads): their
343
+ * stores carry the frozen-object guard. Null without publish calls. */
344
+ publishedTypes;
248
345
  /** Method names with at least one may-throw implementation — the
249
346
  * virtualCall pending check's key. */
250
347
  mayThrowMethods = new Set();
@@ -265,6 +362,11 @@ export class LlEmitter {
265
362
  liveDynUnionRefAdapters = new Map();
266
363
  dynPromiseAdapters = new Map();
267
364
  unionsById = new Map();
365
+ nullableFields;
366
+ /** Unions whose values are nullable arm pointers (nullable-unions.ts). */
367
+ nullableUnions;
368
+ /** The union table without nullable unions: the stack-box analyses. */
369
+ boxedUnionsById;
268
370
  recordsById = new Map();
269
371
  recordCloneShapes = new Set();
270
372
  tracedShapes;
@@ -272,6 +374,8 @@ export class LlEmitter {
272
374
  /** The class graph (buildClassGraph): preorder numbering, hierarchy
273
375
  * membership, virtual slot lists. */
274
376
  classMeta;
377
+ /** Which vtable slots borrow which parameters (virtual-borrows.ts). */
378
+ virtualBorrows;
275
379
  /** Class objects (classes as first-class values): className → the
276
380
  * interned .name literal ref — registered during body emission, the
277
381
  * statics and construct thunks assemble around the bodies. */
@@ -312,6 +416,8 @@ export class LlEmitter {
312
416
  storedSplits = new Map();
313
417
  streamingSplitsEnabled = false;
314
418
  initializerBindings;
419
+ /** Whole-program int32 storage and parameter/result facts. */
420
+ int32Slots;
315
421
  numericLocals = new Map();
316
422
  captureIds = new Set();
317
423
  /** Active induction bindings retain their width independently of size_t. */
@@ -358,18 +464,26 @@ export class LlEmitter {
358
464
  stmt: () => true,
359
465
  expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
360
466
  }));
467
+ this.nullableUnions = new NullableUnions(mod);
361
468
  this.debug =
362
469
  options.debugSources === undefined
363
470
  ? null
364
- : new LlvmDebugInfo(mod.sourceFile, options.debugSources, options.pointerBits, mod.unions);
471
+ : new LlvmDebugInfo(mod.sourceFile, options.debugSources, options.pointerBits, mod.unions, this.nullableUnions.armTypes());
365
472
  this.constantNumericTables = findConstantNumericTables(mod);
366
473
  this.initializerBindings = findInitializerBindings(mod);
474
+ this.int32Slots = analyzeInt32Slots(mod);
367
475
  this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
368
476
  this.ffiExtendNarrowIntegers =
369
477
  options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
370
478
  this.wasi = options.wasi === true;
479
+ this.executableTls =
480
+ mod.workers === true &&
481
+ mod.lib === undefined &&
482
+ !this.wasi &&
483
+ !/apple|darwin|windows|mingw|cygwin|win32/.test(options.targetTriple || process.platform);
371
484
  this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
372
485
  this.runtimeAbiMarker = options.runtimeAbiMarker === true;
486
+ this.rcHelpers = options.inlineRc === true ? new Set() : null;
373
487
  // ScrCycHdr.color stays 12 bytes behind a wasm32 object and 16 bytes
374
488
  // behind a 64-bit object. The wasm32 header pads before color so its
375
489
  // payload remains double-aligned without changing this ABI offset.
@@ -382,11 +496,17 @@ export class LlEmitter {
382
496
  for (const entry of mod.ffiImports ?? []) {
383
497
  this.ffiByName.set(entry.name, entry);
384
498
  }
385
- const mt = computeMayThrow(mod);
499
+ // Executables on Linux and macOS install a native stack guard per thread
500
+ // and per async fiber; WASI, Windows and library builds have none.
501
+ const triple = (options.targetTriple ?? "").toLowerCase();
502
+ const windows = triple === "" ? process.platform === "win32" : triple.includes("windows");
503
+ const mt = computeMayThrow(mod, {
504
+ stackChecks: !this.wasi && mod.lib === undefined && !windows,
505
+ });
386
506
  this.mayThrow = mt.fns;
387
- if (mod.workers)
388
- for (const fn of mod.functions)
389
- this.mayThrow.add(fn.name);
507
+ this.workerEntryPolls = mt.workerEntryPolls ?? null;
508
+ this.stackChecks = mt.stackChecks ?? null;
509
+ this.publishedTypes = computePublishedTypes(mod);
390
510
  this.indirectMayThrow = mt.indirect || mod.workers === true;
391
511
  for (const cls of mod.classes ?? []) {
392
512
  for (const m of cls.methods ?? []) {
@@ -396,12 +516,61 @@ export class LlEmitter {
396
516
  }
397
517
  for (const u of mod.unions ?? [])
398
518
  this.unionsById.set(u.id, u);
519
+ this.boxedUnionsById = this.nullableUnions.boxedUnions(this.unionsById);
520
+ this.immortalValues.add("null");
521
+ this.immortalValues.add(NULLABLE_NULL);
522
+ this.nullableFields = new NullableRefFields(mod.classes ?? [], this.boxedUnionsById);
523
+ // Optional array reads also cover nullable unions: emitLocalArrayRead
524
+ // then yields the (borrowed or owned) element pointer, not a stack box.
399
525
  this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById);
400
- this.referenceEffects = new ReferenceEffects(this.fnByName, (call) => this.optionalArrayReads.get(call) !== null);
401
- this.callLifetimes = analyzeCallLifetimes(this.fnByName);
526
+ this.checkedNarrows = new CheckedNarrows(this.fnByName);
527
+ this.referenceEffects = new ReferenceEffects(this.fnByName, (call) => this.optionalArrayReads.get(call) !== null, mod.classes ?? []);
528
+ const walkParameters = new Map();
529
+ if (this.debug === null) {
530
+ const host = {
531
+ pointerLocal: (local) => this.plainReference(local.type),
532
+ borrowsWithoutOwning: (e) => this.borrowsWithoutOwning(e),
533
+ };
534
+ for (const fn of this.fnByName.values()) {
535
+ if (!this.referenceEffects.functions.has(fn.name))
536
+ continue;
537
+ const walks = findWalkBorrows(fn, host);
538
+ if (walks.size === 0)
539
+ continue;
540
+ this.walkBorrowsByFunction.set(fn.name, walks);
541
+ const indexes = new Set();
542
+ fn.params.forEach((param, index) => {
543
+ if (walks.has(param.localId))
544
+ indexes.add(index);
545
+ });
546
+ if (indexes.size > 0)
547
+ walkParameters.set(fn.name, indexes);
548
+ }
549
+ }
550
+ if (this.debug === null) {
551
+ for (const fn of this.fnByName.values()) {
552
+ // Strings keep the owned parameter: their in-place append
553
+ // (`s = s + x`) needs a uniquely owned binding.
554
+ const rebound = findReboundParameters(fn, (type) => this.plainReference(type) &&
555
+ type.kind !== "string" &&
556
+ (type.kind !== "union" || this.nullableUnions.get(type.unionId)?.arm.kind !== "string"), this.walkBorrowsByFunction.get(fn.name) ?? new Set());
557
+ if (rebound.size === 0)
558
+ continue;
559
+ this.reboundByFunction.set(fn.name, rebound);
560
+ const indexes = walkParameters.get(fn.name) ?? new Set();
561
+ fn.params.forEach((param, index) => {
562
+ if (rebound.has(param.localId))
563
+ indexes.add(index);
564
+ });
565
+ walkParameters.set(fn.name, indexes);
566
+ }
567
+ }
568
+ this.callLifetimes = analyzeCallLifetimes(this.fnByName, (className, field) => this.nullableFields.get(className, field) !== null, walkParameters);
402
569
  this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes);
403
570
  this.stackCallbacks = new StackCallbacks(this.fnByName);
571
+ this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
404
572
  this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
573
+ this.lazyCaptures = new LazyCaptures(this.fnByName);
405
574
  for (const r of mod.records ?? [])
406
575
  this.recordsById.set(r.id, r);
407
576
  const traced = computeTraced(mod);
@@ -412,8 +581,20 @@ export class LlEmitter {
412
581
  cycleColorOffset: this.cycleColorOffset,
413
582
  tracedShapes: this.tracedShapes,
414
583
  tracedUnions: this.tracedUnions,
584
+ nullableUnions: this.nullableUnions,
415
585
  recordsById: this.recordsById,
416
586
  recordCloneShapes: this.recordCloneShapes,
587
+ objectAudit: options.objectAudit !== false,
588
+ inlineAlloc: inlineAllocSupported({
589
+ pointerBits: options.pointerBits,
590
+ wasi: options.wasi,
591
+ targetTriple: options.targetTriple,
592
+ threadInstances: mod.lib?.threadInstances === true,
593
+ }),
594
+ // Worker programs keep one allocator per script thread: the inline
595
+ // paths address its thread-local state.
596
+ threadLocalAlloc: mod.workers === true,
597
+ rcHelpers: this.rcHelpers,
417
598
  unionsById: this.unionsById,
418
599
  declare: (decl) => this.declare(decl),
419
600
  needOom: () => this.needOom(),
@@ -421,6 +602,8 @@ export class LlEmitter {
421
602
  internLiteral: (text) => this.internLiteral(text),
422
603
  cstr: (text) => this.cstr(text),
423
604
  unitInstanceRef: (unionId, tag) => this.unitInstanceRef(unionId, tag),
605
+ fieldAbsentTestIn: (B, value, unionId) => this.fieldAbsentTestIn(B, value, unionId),
606
+ absentInstanceRef: (unionId) => this.absentInstanceRef(unionId),
424
607
  liveDynRefAdapter: (type) => this.liveDynRefAdapter(type),
425
608
  liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type),
426
609
  dynPromiseAdapter: (type) => this.dynPromiseAdapter(type),
@@ -432,6 +615,8 @@ export class LlEmitter {
432
615
  .filter((meta) => intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post))
433
616
  .map((meta) => meta.def.name);
434
617
  },
618
+ pendingTestLines: (dest) => this.pendingTestLines(dest),
619
+ virtualEntry: (implFn) => this.virtualEntry(implFn),
435
620
  };
436
621
  this.walkers = new LlWalkers(this.shapeHost);
437
622
  this.dyn = new LlDyn(this.shapeHost);
@@ -488,6 +673,21 @@ export class LlEmitter {
488
673
  lib: rec.lib,
489
674
  });
490
675
  }
676
+ this.virtualBorrows = new VirtualBorrows(mod, this.classMeta, this.fnByName, this.callLifetimes.borrowed);
677
+ // After the vtable slots: borrowing slots exclude their implementations.
678
+ if (this.debug === null)
679
+ this.analyzeBorrowedReturns();
680
+ }
681
+ /** The borrowed parameters of a hierarchy's vtable slot (empty: owned). */
682
+ virtualSlotBorrowed(rootName, slotIndex) {
683
+ return this.virtualBorrows.slot(rootName, slotIndex);
684
+ }
685
+ /** The vtable entry for one implementation (VirtualBorrows.entry). */
686
+ virtualEntry(implFn) {
687
+ return this.virtualBorrows.entry(implFn);
688
+ }
689
+ implSlotBorrowed(implFn) {
690
+ return this.virtualBorrows.implSlotBorrowed(implFn);
491
691
  }
492
692
  abiOffset(native64, wasm32) {
493
693
  return this.sizeType === "i32" ? wasm32 : native64;
@@ -528,7 +728,6 @@ export class LlEmitter {
528
728
  const defs = [];
529
729
  if (this.ffiCallbackAdapters.size === 0)
530
730
  return { globals, defs };
531
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
532
731
  this.declare(`declare void @scr_trap(ptr)`);
533
732
  const expired = this.cstr("scriptc: native callback invoked outside its call-scoped lifetime\n");
534
733
  const released = this.cstr("scriptc: native callback invoked outside its retained lifetime\n");
@@ -646,7 +845,7 @@ export class LlEmitter {
646
845
  ? ` %cb = load ptr, ptr @${adapter.tls}`
647
846
  : adapter.global !== null
648
847
  ? ` %cb = load ptr, ptr @${adapter.global}`
649
- : ` %cb = getelementptr inbounds i8, ptr %ctx, i64 0`, ` %missing = icmp eq ptr %cb, null`, ` br i1 %missing, label %expired, label %ready`, `expired:`, ` call void @scr_trap(ptr ${adapter.callback.lifetime === "call" ? expired : released})`, ` unreachable`, `ready:`, ` %pending = call zeroext i1 @scr_exc_pending()`, ` br i1 %pending, label %skip, label %invoke`, `skip:`, ` ret ${ffiCallbackDummyLl(cb)}`, `invoke:`);
848
+ : ` %cb = getelementptr inbounds i8, ptr %ctx, i64 0`, ` %missing = icmp eq ptr %cb, null`, ` br i1 %missing, label %expired, label %ready`, `expired:`, ` call void @scr_trap(ptr ${adapter.callback.lifetime === "call" ? expired : released})`, ` unreachable`, `ready:`, ...this.pendingTestLines("%pending"), ` br i1 %pending, label %skip, label %invoke`, `skip:`, ` ret ${ffiCallbackDummyLl(cb)}`, `invoke:`);
650
849
  // Validate every native pointer before allocating any copy-in value.
651
850
  // A later bad slot therefore cannot leak an earlier materialization.
652
851
  for (let i = 0; i < cb.params.length; i++) {
@@ -794,11 +993,28 @@ export class LlEmitter {
794
993
  return this.emitParts().join("\n");
795
994
  }
796
995
  emitParts() {
996
+ const parts = this.emitModuleParts();
997
+ if (!this.executableTls)
998
+ return parts;
999
+ // A worker executable's thread-locals all live in the executable's own
1000
+ // TLS block (the program's internal ones, the runtime's external ones),
1001
+ // so on ELF they can use the executable models: LLVM's default
1002
+ // general-dynamic sequence (relaxed by the linker to `mov %fs:0` plus
1003
+ // an add) becomes one %fs-relative access LLVM can fold into loads.
1004
+ // Darwin's thread-local variables have a single model.
1005
+ return parts.map((part) => part
1006
+ .replace(/^(@[-$._A-Za-z0-9]+ = internal )thread_local global /gm, "$1thread_local(localexec) global ")
1007
+ .replace(/^(@[-$._A-Za-z0-9]+ = external )thread_local global /gm, "$1thread_local(initialexec) global "));
1008
+ }
1009
+ emitModuleParts() {
797
1010
  // Function bodies first (the literal/unit/fn-value tables fill as they
798
1011
  // emit), then the file assembles around them — the runtime ABI’s order.
799
1012
  const fnDefs = [];
800
- for (const fn of this.mod.functions)
801
- fnDefs.push(this.emitFunction(fn));
1013
+ for (const fn of this.mod.functions) {
1014
+ const def = this.emitFunction(fn);
1015
+ fnDefs.push(def);
1016
+ this.fnDefText.set(fn.name, def);
1017
+ }
802
1018
  const errorViews = [];
803
1019
  for (const property of ["name", "message"])
804
1020
  if (this.mod.functions.some((fn) => fn.name === `%error.${property}.read`)) {
@@ -817,7 +1033,7 @@ export class LlEmitter {
817
1033
  this.declare(`declare void @scr_error_register_dyn(ptr, ptr)`);
818
1034
  errorViews.push(` call void @scr_error_register_dyn(ptr @${mangleVtInstance(meta.def.name)}, ptr @${name})`);
819
1035
  }
820
- const layouts = emitLlvmLayouts(this.shapeHost, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t));
1036
+ const layouts = emitLlvmLayouts(this.shapeHost, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t), this.nullableFields, this.int32Slots);
821
1037
  const shapes = layouts.records;
822
1038
  const classShapes = layouts.classes;
823
1039
  const classObjDefs = layouts.classObjects;
@@ -1020,12 +1236,15 @@ export class LlEmitter {
1020
1236
  const topPendingReleases = asyncEntry ? globalReleaseLines("gp") : [];
1021
1237
  const loopReportedReleases = runsLoop ? globalReleaseLines("gq") : [];
1022
1238
  // main's epilogues read the flag / the loop entry points — declared
1023
- // HERE, before the extern block flushes (a pending check usually
1024
- // declared the flag already; the Set dedupes).
1239
+ // HERE, before the extern block flushes. They run once, so they keep
1240
+ // the out-of-line scr_exc_pending call; per-call checks inline the
1241
+ // test (pendingTestLines).
1025
1242
  if (entryMayThrow || runsLoop)
1026
1243
  this.declare(`declare zeroext i1 @scr_exc_pending()`);
1027
1244
  if (runsLoop) {
1028
1245
  this.declare(`declare zeroext i1 @scr_loop_run(ptr)`);
1246
+ if (this.mod.commonJsEntry === true)
1247
+ this.declare(`declare void @scr_loop_commonjs_entry()`);
1029
1248
  this.declare(`declare zeroext i1 @scr_report_unhandled_rejections()`);
1030
1249
  this.declare(`declare void @scr_discard_unhandled_rejections()`);
1031
1250
  }
@@ -1116,6 +1335,8 @@ export class LlEmitter {
1116
1335
  const helpers = [
1117
1336
  ...this.helperDefs(),
1118
1337
  ...emitClassMembershipHelper(this.classMeta, this.sizeType),
1338
+ ...(this.publishWalkersCache?.defs() ?? []),
1339
+ ...this.publishedClassNamesDefs(),
1119
1340
  ];
1120
1341
  if (this.constantNumericTables.size > 0)
1121
1342
  this.declare(`declare double @scr_arr_get_number(ptr, double)`);
@@ -1132,13 +1353,28 @@ export class LlEmitter {
1132
1353
  `%ScrLogArg = type { i32, i64 }`,
1133
1354
  `%ScrVt = type { ${this.sizeType}, ${this.sizeType}, ptr }`,
1134
1355
  `%ScrUnion = type { ${this.sizeType}, i32, ptr, ptr, ptr, i64 }`,
1135
- `%ScrClosure = type { ${this.sizeType}, ptr, ${this.sizeType}, ptr, i32 }`,
1356
+ `%ScrClosure = type { ${this.sizeType}, ptr, ${this.sizeType}, ptr, i32, i32, ptr }`,
1136
1357
  `%ScrFfiTable = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, i8, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`,
1137
- `%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr, double, ptr }`,
1358
+ `%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr, double, ptr, ptr }`,
1138
1359
  // ScrArr mirrors scr_runtime.h field-for-field. Live dynamic stream
1139
1360
  // commits swap its mutable dense, sparse, presence, and property
1140
1361
  // storage while preserving the target object's identity.
1141
1362
  `%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, ptr, ptr, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ptr, ${this.sizeType}, ${this.sizeType}, ptr }`,
1363
+ // The ScrMap prefix { rc, key_kind, val_kind, val_retain, val_release,
1364
+ // val_trace, key_retain, key_release, key_trace } — the inline RC
1365
+ // fast paths test the two trace slots (shapes.ts).
1366
+ ...(this.rcHelpers === null
1367
+ ? []
1368
+ : [`%ScrMapRc = type { ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr, ptr, ptr }`]),
1369
+ // The complete ScrMap and its { key, val, hash } entry, read by the
1370
+ // inline number-key lookup (sc_map_entry_f64): entries is field 12,
1371
+ // the direct index and its length fields 18 and 19.
1372
+ ...(this.decls.has(NUMBER_MAP_ENTRY_DECL)
1373
+ ? [
1374
+ `%ScrMapIx = type { ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ${this.sizeType}, ptr, ${this.sizeType}, ptr, ptr, ptr, ${this.sizeType} }`,
1375
+ `%ScrMapEntry = type { i64, i64, i64 }`,
1376
+ ]
1377
+ : []),
1142
1378
  // The runtime error prefix { rc, vt, name, message, code, cause } and the
1143
1379
  // class-object shape { rc, pre, post, ctor, name } — field reads on
1144
1380
  // builtin errors and classval loads GEP through these.
@@ -1184,12 +1420,13 @@ export class LlEmitter {
1184
1420
  // data (string literals, unit arms, template arrays, vtables) stays
1185
1421
  // shared.
1186
1422
  const tl = this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : "";
1187
- out.push(``, `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, `declare void @scr_init()`, `declare void @scr_lib_init(i32, ptr)`, ...(this.mod.workers ? [`declare void @scr_runtime_workers_v7()`] : []), ...(this.runtimeAbiMarker && this.mod.lib === undefined
1423
+ out.push(``, `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, `declare void @scr_init()`, `declare void @scr_lib_init(i32, ptr)`, ...(this.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []), ...(this.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []), ...(this.runtimeAbiMarker && this.mod.lib === undefined
1188
1424
  ? [`declare void @${RUNTIME_ABI_MARKER}()`]
1189
1425
  : []));
1190
1426
  for (const d of this.decls)
1191
1427
  out.push(d);
1192
1428
  out.push(``);
1429
+ const flushedDecls = new Set(this.decls);
1193
1430
  for (const table of this.constantNumericTables.values()) {
1194
1431
  const n = table.values.length;
1195
1432
  out.push(`@${table.symbol} = private constant [${n} x double] [${table.values.map((v) => `double ${f64Lit(v)}`).join(", ")}]`, `define internal double @${table.symbol}_get(ptr %a, double %i) alwaysinline #0 {`, `entry:`, ` %initialized = icmp ne ptr %a, null`, ` %nonnegative = fcmp oge double %i, ${f64Lit(0)}`, ` %below = fcmp olt double %i, ${f64Lit(n)}`, ` %range = and i1 %nonnegative, %below`, ` %safe = and i1 %initialized, %range`, ` br i1 %safe, label %convert, label %fallback`, `convert:`,
@@ -1200,8 +1437,14 @@ export class LlEmitter {
1200
1437
  for (const [text, lit] of this.literals) {
1201
1438
  // Immortal interned ScrStr: { rc = SIZE_MAX, len, cap = len, bytes\0 } —
1202
1439
  // the runtime ABI’s static table, retain/release skip rc == SIZE_MAX.
1440
+ // 64-bit (little-endian) targets split the capacity word: the high
1441
+ // half carries the precomputed Map key hash (SCR_STR_HASH_CACHE).
1442
+ // Bit 31 marks all-ASCII literals (SCR_STR_ASCII_BIT) on every target.
1443
+ const capWord = this.sizeType === "i64"
1444
+ ? literalCapWord(Buffer.from(text, "utf8"))
1445
+ : literalCapWord32(Buffer.from(text, "utf8"));
1203
1446
  out.push(`@${lit.sym} = internal global { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, [${lit.len + 1} x i8] } ` +
1204
- `{ ${this.sizeType} -1, ${this.sizeType} ${lit.len}, ${this.sizeType} ${lit.len}, [${lit.len + 1} x i8] c"${llStrBytes(text)}" }`);
1447
+ `{ ${this.sizeType} -1, ${this.sizeType} ${lit.len}, ${this.sizeType} ${capWord}, [${lit.len + 1} x i8] c"${llStrBytes(text)}" }`);
1205
1448
  }
1206
1449
  if (this.literals.size > 0)
1207
1450
  out.push(``);
@@ -1214,11 +1457,37 @@ export class LlEmitter {
1214
1457
  }
1215
1458
  if (this.unitInstances.size > 0)
1216
1459
  out.push(``);
1460
+ for (const [unionId, sym] of this.absentInstances) {
1461
+ // One immortal ABSENT field-slot instance per undefined-armed union:
1462
+ // the undefined arm's tag with payload 1 (ordinary unit instances
1463
+ // carry 0). Every tag reader sees undefined; recordHas and the
1464
+ // field reads compare tag and payload, never the address, so the
1465
+ // per-object copies of separately compiled units agree.
1466
+ const tag = undefinedArmTag({ kind: "union", unionId }, this.unionsById);
1467
+ out.push(`@${sym} = internal global %ScrUnion { ${this.sizeType} -1, i32 ${tag}, ptr null, ptr null, ptr null, i64 ${ABSENT_FIELD_PAYLOAD} } ; ${unionId} absent field`);
1468
+ }
1469
+ if (this.absentInstances.size > 0)
1470
+ out.push(``);
1471
+ if (this.nullableUnions.usesNullSentinel) {
1472
+ // `null` in every `C | null | undefined` union (NULL is undefined):
1473
+ // one module-scope immortal object like the ABSENT sentinel.
1474
+ const S = this.sizeType;
1475
+ out.push(`${NULLABLE_NULL} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable null arm`, ``);
1476
+ }
1477
+ if (this.needsNullableAbsent) {
1478
+ // The ABSENT field-slot state of every nullable-pointer union: one
1479
+ // module-scope immortal object (rc == SIZE_MAX), distinct from NULL
1480
+ // (undefined) and from every instance. RC entry points and the
1481
+ // collector skip it like any immortal; field reads compare its address.
1482
+ const S = this.sizeType;
1483
+ out.push(`${NULLABLE_ABSENT} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable absent field`, ``);
1484
+ }
1217
1485
  for (const [key, re] of this.regexInstances) {
1218
1486
  // One immortal ScrRegex per (pattern, flags) literal, pointing at
1219
- // the interned source/flags strings. The bc slot starts null (lazy
1220
- // compile, cached by the runtime) — a mutable global, not constant.
1221
- out.push(`@${re.sym} = internal ${tl}global %ScrRegex { ${this.sizeType} -1, ptr ${re.src}, ptr ${re.fl}, ptr null, double 0.0, ptr null } ; ${key.replace(/\n/g, "\\n")}`);
1487
+ // the interned source/flags strings. The bc and native-matcher slots
1488
+ // start null (lazy compile, cached by the runtime) — a mutable
1489
+ // global, not constant.
1490
+ out.push(`@${re.sym} = internal ${tl}global %ScrRegex { ${this.sizeType} -1, ptr ${re.src}, ptr ${re.fl}, ptr null, double 0.0, ptr null, ptr null } ; ${key.replace(/\n/g, "\\n")}`);
1222
1491
  }
1223
1492
  if (this.regexInstances.size > 0)
1224
1493
  out.push(``);
@@ -1367,6 +1636,7 @@ export class LlEmitter {
1367
1636
  // symbols specified by the library IR instead.
1368
1637
  for (const line of this.emitLibDefs(globals, globalReleaseLines, stamps))
1369
1638
  out.push(line);
1639
+ out.push(...this.inlineRcTail(flushedDecls));
1370
1640
  out.push(`attributes #0 = { sanitize_address }`);
1371
1641
  if (this.wasi)
1372
1642
  out.push(`attributes #1 = { sanitize_address presplitcoroutine }`);
@@ -1378,7 +1648,10 @@ export class LlEmitter {
1378
1648
  out.push(this.debug.render());
1379
1649
  return out;
1380
1650
  }
1381
- out.push(`define i32 @${this.mod.workers ? "sc_context_entry" : this.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`, `entry:`, ...(this.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []), ...(this.mod.workers ? [` call void @scr_runtime_workers_v7()`] : []), ` call void @scr_init()`, ...stamps,
1651
+ out.push(`define i32 @${this.mod.workers ? "sc_context_entry" : this.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`, `entry:`, ...(this.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []), ...(this.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []), ` call void @scr_init()`,
1652
+ // Each context (the main thread, every worker) installs its stack
1653
+ // guard before running compiled code that checks it.
1654
+ ...(this.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []), ...stamps,
1382
1655
  // Event-surface programs (signal/exit listeners) fill the loop's
1383
1656
  // nullable event hooks before %main — scr_events.c links only when
1384
1657
  // this line is emitted (native/executable.ts gates on the same predicate).
@@ -1423,6 +1696,7 @@ export class LlEmitter {
1423
1696
  // both exit 1, like Node — the C main's loop block exactly.
1424
1697
  ...(runsLoop
1425
1698
  ? [
1699
+ ...(this.mod.commonJsEntry === true ? [` call void @scr_loop_commonjs_entry()`] : []),
1426
1700
  ` %loop_rejection = call zeroext i1 @scr_loop_run(ptr ${asyncEntry ? "%top" : "null"})`,
1427
1701
  ` %lexc = call zeroext i1 @scr_exc_pending()`,
1428
1702
  ` br i1 %lexc, label %luncaught, label %lok`,
@@ -1486,7 +1760,7 @@ export class LlEmitter {
1486
1760
  ? [` %test_exit = call i32 @scr_test_exit_code()`, ` ret i32 %test_exit`]
1487
1761
  : usesIsland
1488
1762
  ? [` %island_exit = call i32 @scr_island_exit_code()`, ` ret i32 %island_exit`]
1489
- : [` %native_exit = call i32 @scr_exit_code_hint_get()`, ` ret i32 %native_exit`]), `}`, ``,
1763
+ : [` %native_exit = call i32 @scr_exit_code_hint_get()`, ` ret i32 %native_exit`]), `}`, ``, ...this.inlineRcTail(flushedDecls),
1490
1764
  // sanitize_address is inert under the plain pipeline; the sanitized
1491
1765
  // lane's -fsanitize=address link activates instrumentation over the
1492
1766
  // emitted functions too (the runtime TUs get theirs from clang).
@@ -1738,15 +2012,96 @@ export class LlEmitter {
1738
2012
  msgHelper("sc_bad_key", "sc_bad_key_msg", "scriptc: TypeError: record has no key (typed slot — no undefined is representable)\n");
1739
2013
  }
1740
2014
  if (this.needsRetainBox) {
1741
- // scr_box_retain is a static inline (increment-unless-immortal, then
1742
- // mark the cycle header live — every box is collector-headered);
2015
+ // scr_box_retain is a static inline (increment-unless-immortal);
1743
2016
  // emitted once with internal linkage, like the record retains.
1744
- defs.push(`define internal ptr @sc_retain_box(ptr %b) ${FN_ATTRS} {`, `entry:`, ` %rc = load ${this.sizeType}, ptr %b`, ` %imm = icmp eq ${this.sizeType} %rc, -1`, ` br i1 %imm, label %done, label %inc`, `inc:`, ` %n = add ${this.sizeType} %rc, 1`, ` store ${this.sizeType} %n, ptr %b`, ` %colorp = getelementptr i8, ptr %b, ${this.sizeType} -${this.cycleColorOffset}`, ` store i32 0, ptr %colorp ; mark live`, ` br label %done`, `done:`, ` ret ptr %b`, `}`, ``);
2017
+ defs.push(`define internal ptr @sc_retain_box(ptr %b) ${FN_ATTRS} {`, `entry:`, ` %rc = load ${this.sizeType}, ptr %b`, ` %imm = icmp eq ${this.sizeType} %rc, -1`, ` br i1 %imm, label %done, label %inc`, `inc:`, ` %n = add ${this.sizeType} %rc, 1`, ` store ${this.sizeType} %n, ptr %b`, ` br label %done`, `done:`, ` ret ptr %b`, `}`, ``);
2018
+ }
2019
+ // Number-key Map/Set lookups probe the runtime's direct index inline:
2020
+ // while ndense is nonzero every live key is an integer below it, so an
2021
+ // in-range integral key reads its slot (entry index + 1, 0 = absent) and
2022
+ // any other key is absent. Maps without the index (ndense 0) and every
2023
+ // non-integral probe outside the table take the runtime lookup, which
2024
+ // keeps the complete SameValueZero semantics.
2025
+ if (this.decls.has(NUMBER_MAP_ENTRY_DECL)) {
2026
+ const sz = this.sizeType;
2027
+ defs.push(`define internal ptr @sc_map_entry_f64(ptr %m, double %k) ${FN_ATTRS} {`, `entry:`, ` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 19`, ` %nd = load ${sz}, ptr %ndp`, ` %ndf = uitofp ${sz} %nd to double`, ` %lo = fcmp oge double %k, 0.0`, ` %hi = fcmp olt double %k, %ndf`, ` %in = and i1 %lo, %hi`, ` br i1 %in, label %direct, label %outside`, `direct:`, ` %i = fptoui double %k to ${sz}`, ` %back = uitofp ${sz} %i to double`, ` %int = fcmp oeq double %back, %k`, ` br i1 %int, label %slot, label %miss`, `slot:`, ` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 18`, ` %dense = load ptr, ptr %dp`, ` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`, ` %s = load i32, ptr %sp`, ` %empty = icmp eq i32 %s, 0`, ` br i1 %empty, label %miss, label %hit`, `hit:`, ` %e32 = sub i32 %s, 1`, sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`, ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 12`, ` %entries = load ptr, ptr %ep`, ` %found = getelementptr inbounds %ScrMapEntry, ptr %entries, ${sz} %e`, ` ret ptr %found`, `miss:`, ` ret ptr null`, `outside:`, ` %dense.any = icmp ne ${sz} %nd, 0`, ` br i1 %dense.any, label %miss, label %slow`, `slow:`, ` %r = call ptr @scr_map_entry_f64(ptr %m, double %k)`, ` ret ptr %r`, `}`, ``);
2028
+ }
2029
+ // String comparisons enter through small internal wrappers that LLVM
2030
+ // inlines at each site: interned literals and shared values compare by
2031
+ // identity, and equality rejects different byte lengths, before any
2032
+ // runtime call (scr_str_eq/scr_str_cmp keep the complete semantics).
2033
+ if (this.decls.has(`declare zeroext i1 @scr_str_eq(ptr, ptr)`)) {
2034
+ defs.push(`define internal zeroext i1 @sc_str_eq(ptr %a, ptr %b) ${FN_ATTRS} {`, `entry:`, ` %same = icmp eq ptr %a, %b`, ` br i1 %same, label %yes, label %lengths`, `lengths:`, ` %alen.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 1`, ` %blen.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 1`, ` %alen = load ${this.sizeType}, ptr %alen.p`, ` %blen = load ${this.sizeType}, ptr %blen.p`, ` %samelen = icmp eq ${this.sizeType} %alen, %blen`, ` br i1 %samelen, label %bytes, label %no`, `bytes:`, ` %eq = call zeroext i1 @scr_str_eq(ptr %a, ptr %b)`, ` ret i1 %eq`, `yes:`, ` ret i1 true`, `no:`, ` ret i1 false`, `}`, ``);
2035
+ }
2036
+ // String length and charCodeAt read proven-ASCII strings inline: bit 31
2037
+ // of the capacity word (SCR_STR_ASCII_BIT, the low half's sign bit on
2038
+ // little-endian targets) makes UTF-16 units bytes. Unproven strings and
2039
+ // anything but an in-range index take the runtime's UTF-16 mapping.
2040
+ for (const helper of STRING_READ_HELPERS) {
2041
+ if (!this.decls.has(helper.decl))
2042
+ continue;
2043
+ defs.push(...helper.define(this.sizeType, FN_ATTRS), ``);
2044
+ }
2045
+ // Ordering also decides inline when the first bytes differ: byte order
2046
+ // is the plain comparison's order, and the UTF-16 order whenever both
2047
+ // bytes are ASCII. Callers only test the sign of the result.
2048
+ for (const fn of ["scr_str_cmp", "scr_str_cmp_u16"]) {
2049
+ if (!this.decls.has(`declare i32 @${fn}(ptr, ptr)`))
2050
+ continue;
2051
+ this.declare("declare i64 @llvm.bswap.i64(i64)");
2052
+ const utf16 = fn === "scr_str_cmp_u16";
2053
+ defs.push(`define internal i32 @${fn.replace(/^scr_/, "sc_")}(ptr %a, ptr %b) ${FN_ATTRS} {`, `entry:`, ` %same = icmp eq ptr %a, %b`, ` br i1 %same, label %equal, label %lengths`, `lengths:`, ` %alen.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 1`, ` %blen.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 1`, ` %alen = load ${this.sizeType}, ptr %alen.p`, ` %blen = load ${this.sizeType}, ptr %blen.p`, ` %aempty = icmp eq ${this.sizeType} %alen, 0`, ` %bempty = icmp eq ${this.sizeType} %blen, 0`, ` %empty = or i1 %aempty, %bempty`, ` br i1 %empty, label %order, label %words`,
2054
+ // Both at least 8 bytes: the first differing byte of one word pair.
2055
+ // Byte-swapped little-endian words compare in byte order.
2056
+ `words:`, ` %along = icmp uge ${this.sizeType} %alen, 8`, ` %blong = icmp uge ${this.sizeType} %blen, 8`, ` %long = and i1 %along, %blong`, ` br i1 %long, label %word, label %first`, `word:`, ` %awp = getelementptr inbounds %ScrStr, ptr %a, i32 1`, ` %bwp = getelementptr inbounds %ScrStr, ptr %b, i32 1`, ` %aw = load i64, ptr %awp, align 1`, ` %bw = load i64, ptr %bwp, align 1`, ` %wdiffer = icmp ne i64 %aw, %bw`, ` br i1 %wdiffer, label %wdiff, label %wsame`, `wdiff:`, ...(utf16
2057
+ ? [
2058
+ ` %wbits = or i64 %aw, %bw`,
2059
+ ` %whigh = and i64 %wbits, -9187201950435737472`,
2060
+ ` %wascii = icmp eq i64 %whigh, 0`,
2061
+ ` br i1 %wascii, label %wordorder, label %order`,
2062
+ ]
2063
+ : [` br label %wordorder`]), `wordorder:`, ` %asw = call i64 @llvm.bswap.i64(i64 %aw)`, ` %bsw = call i64 @llvm.bswap.i64(i64 %bw)`, ` %wbelow = icmp ult i64 %asw, %bsw`, ` %wsign = select i1 %wbelow, i32 -1, i32 1`, ` ret i32 %wsign`,
2064
+ // Equal first words and one length of at most 16 bytes: the word
2065
+ // ending at that length covers the rest, and any difference lies
2066
+ // past the equal first word.
2067
+ `wsame:`, ` %samelen = icmp eq ${this.sizeType} %alen, %blen`, ` %short = icmp ule ${this.sizeType} %alen, 16`, ` %tailable = and i1 %samelen, %short`, ` br i1 %tailable, label %tail, label %order`, `tail:`, ` %toff = sub ${this.sizeType} %alen, 8`, ` %atp = getelementptr inbounds i8, ptr %awp, ${this.sizeType} %toff`, ` %btp = getelementptr inbounds i8, ptr %bwp, ${this.sizeType} %toff`, ` %at = load i64, ptr %atp, align 1`, ` %bt = load i64, ptr %btp, align 1`, ` %tdiffer = icmp ne i64 %at, %bt`, ` br i1 %tdiffer, label %tdiff, label %equal`, `tdiff:`, ...(utf16
2068
+ ? [
2069
+ ` %tbits = or i64 %at, %bt`,
2070
+ ` %thigh = and i64 %tbits, -9187201950435737472`,
2071
+ ` %tascii = icmp eq i64 %thigh, 0`,
2072
+ ` br i1 %tascii, label %tailorder, label %order`,
2073
+ ]
2074
+ : [` br label %tailorder`]), `tailorder:`, ` %ats = call i64 @llvm.bswap.i64(i64 %at)`, ` %bts = call i64 @llvm.bswap.i64(i64 %bt)`, ` %tbelow = icmp ult i64 %ats, %bts`, ` %tsign = select i1 %tbelow, i32 -1, i32 1`, ` ret i32 %tsign`, `first:`, ` %adata = getelementptr inbounds %ScrStr, ptr %a, i32 1`, ` %bdata = getelementptr inbounds %ScrStr, ptr %b, i32 1`, ` %x = load i8, ptr %adata`, ` %y = load i8, ptr %bdata`, ` %differ = icmp ne i8 %x, %y`, ...(utf16
2075
+ ? [
2076
+ ` %bits = or i8 %x, %y`,
2077
+ ` %ascii = icmp sge i8 %bits, 0`,
2078
+ ` %decide = and i1 %differ, %ascii`,
2079
+ ]
2080
+ : [` %decide = and i1 %differ, true`]), ` br i1 %decide, label %byte, label %order`, `byte:`, ` %below = icmp ult i8 %x, %y`, ` %sign = select i1 %below, i32 -1, i32 1`, ` ret i32 %sign`, `order:`, ` %c = call i32 @${fn}(ptr %a, ptr %b)`, ` ret i32 %c`, `equal:`, ` ret i32 0`, `}`, ``);
1745
2081
  }
1746
2082
  // The declarations these helpers added must land in the extern block,
1747
2083
  // which already flushed — append here instead (LLVM is order-free).
1748
2084
  return defs.length > 0 ? [...defs] : defs;
1749
2085
  }
2086
+ /** The inline RC helpers every call site requested, emitted last so
2087
+ * requests from main/library epilogues are included. Slow-path
2088
+ * declarations a late request added after the extern block flushed land
2089
+ * here too. */
2090
+ inlineRcTail(flushedDecls) {
2091
+ const late = inlineRcDecls(this.shapeHost).filter((d) => !flushedDecls.has(d));
2092
+ // NULL-skipping retain wrappers of nullable unions; a runtime inner
2093
+ // retain requested after the extern block flushed is declared here.
2094
+ for (const inner of this.nullableUnions.retainWrappers.values()) {
2095
+ const decl = `declare ptr ${inner}(ptr)`;
2096
+ if (inner.startsWith("@scr_") && !flushedDecls.has(decl) && !late.includes(decl))
2097
+ late.push(decl);
2098
+ }
2099
+ return [
2100
+ ...late,
2101
+ ...emitNullableRetainWrappers(this.shapeHost),
2102
+ ...emitInlineRcHelpers(this.shapeHost),
2103
+ ];
2104
+ }
1750
2105
  /** Env-signature wrappers + interned immortal closures for declared
1751
2106
  * functions used as values (the runtime ABI’s sc_w_/sc_fc_ pair): every
1752
2107
  * mention of `f` yields the same pointer, so `f === f` holds. */
@@ -1761,7 +2116,7 @@ export class LlEmitter {
1761
2116
  // never the inner return type.
1762
2117
  const ret = fn.async === true || fn.generator !== undefined ? "ptr" : this.llType(fn.returnType);
1763
2118
  const call = `call ${ret} @${this.callTarget(name)}(${args})`;
1764
- out.push(`define internal ${ret} @${mangleWrapper(name)}(ptr %env${params.length ? ", " + params.join(", ") : ""}) ${FN_ATTRS} { ; ${name} as a value`, `entry:`, ret === "void" ? ` ${call}` : ` %r = ${call}`, ret === "void" ? ` ret void` : ` ret ${ret} %r`, `}`, `@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${(fn.generator ? 1 : 0) + (fn.async ? 2 : 0) + (fn.ownsPrototype ? 4 : 0)} }`, ``);
2119
+ out.push(`define internal ${ret} @${mangleWrapper(name)}(ptr %env${params.length ? ", " + params.join(", ") : ""}) ${FN_ATTRS} { ; ${name} as a value`, `entry:`, ret === "void" ? ` ${call}` : ` %r = ${call}`, ret === "void" ? ` ret void` : ` ret ${ret} %r`, `}`, `@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${(fn.generator ? 1 : 0) + (fn.async ? 2 : 0) + (fn.ownsPrototype ? 4 : 0)}, i32 0, ptr null }`, ``);
1765
2120
  }
1766
2121
  return out;
1767
2122
  }
@@ -1776,7 +2131,6 @@ export class LlEmitter {
1776
2131
  const sizeOf = `ptrtoint (ptr getelementptr (%${pack}, ptr null, i32 1) to ${this.sizeType})`;
1777
2132
  this.declare(`declare void @free(ptr)`);
1778
2133
  this.declare(`declare ptr @malloc(${this.sizeType})`);
1779
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
1780
2134
  const tr = [
1781
2135
  `define internal void @${mangleTrampoline(fn.name)}(ptr %self, ptr %ap) ${FN_ATTRS} {`,
1782
2136
  `entry:`,
@@ -1815,6 +2169,153 @@ export class LlEmitter {
1815
2169
  });
1816
2170
  return { definitions, pack, lifted, fieldTys, ret, tr, spawnParams, argPackLines };
1817
2171
  }
2172
+ /** Settles `promise` from a clean async body result `%r` (moved in). */
2173
+ asyncFulfillLines(ret, promise) {
2174
+ switch (ret.kind) {
2175
+ case "void":
2176
+ this.declare(`declare void @scr_promise_fulfill_void(ptr)`);
2177
+ return [` call void @scr_promise_fulfill_void(ptr ${promise})`];
2178
+ case "f64":
2179
+ case "date":
2180
+ this.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`);
2181
+ return [` call void @scr_promise_fulfill_f64(ptr ${promise}, double %r)`];
2182
+ case "bool":
2183
+ this.declare(`declare void @scr_promise_fulfill_bool(ptr, i1 zeroext)`);
2184
+ return [` call void @scr_promise_fulfill_bool(ptr ${promise}, i1 %r)`];
2185
+ case "string":
2186
+ this.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`);
2187
+ return [` call void @scr_promise_fulfill_str(ptr ${promise}, ptr %r) ; moves in`];
2188
+ case "dyn":
2189
+ this.declare(`declare void @scr_promise_resolve_dyn(ptr, ptr)`);
2190
+ return [` call void @scr_promise_resolve_dyn(ptr ${promise}, ptr %r) ; moves in`];
2191
+ default: {
2192
+ const v = vAdapters(this.shapeHost, ret);
2193
+ this.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
2194
+ return [
2195
+ ` call void @scr_promise_fulfill_ref(ptr ${promise}, ptr %r, ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(this.shapeHost, ret)})`,
2196
+ ];
2197
+ }
2198
+ }
2199
+ }
2200
+ /** Async functions whose calls can skip the fiber (scr_async_inline_*).
2201
+ * A body qualifies only when nothing it runs can park its execution
2202
+ * context: its emitted text, and the text of every module function it
2203
+ * mentions (transitively), contains no suspension entry point. Such a
2204
+ * body always completes before the call returns, which is exactly the
2205
+ * behavior of a fiber that never suspends. Excluded conservatively:
2206
+ * wasm32 (its coroutine lowering owns async bodies), programs embedding
2207
+ * the dynamic engine, module-evaluation bodies (their promise caches),
2208
+ * `dyn` results (resolution may adopt a thenable), and every program
2209
+ * that uses node:test subtests — `t.test()` runs a subtest inline and
2210
+ * awaits it on the CURRENT fiber even from synchronous code. Worker
2211
+ * programs keep fibers too: their fiber epilogue owns context
2212
+ * termination (a stopping worker rejects and observes the promise), which
2213
+ * the fiberless wrapper does not reproduce. A missed suspension still
2214
+ * cannot reorder silently: the runtime traps when a context parks while a
2215
+ * fiberless body is on its stack. */
2216
+ fiberlessAsyncFunctions() {
2217
+ const eligible = new Set();
2218
+ if (this.wasi || this.mod.workers === true)
2219
+ return eligible;
2220
+ const embedded = this.mod.embedded;
2221
+ if (embedded !== undefined && embedded.modules.length > 0)
2222
+ return eligible;
2223
+ const suspends = /@scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_)/;
2224
+ for (const decl of this.decls)
2225
+ if (/@scr_test_sub\b/.test(decl))
2226
+ return eligible;
2227
+ for (const text of this.fnDefText.values())
2228
+ if (/@scr_test_sub\b/.test(text))
2229
+ return eligible;
2230
+ const byMangled = new Map();
2231
+ for (const fn of this.mod.functions) {
2232
+ byMangled.set(mangleFunction(fn.name), fn.name);
2233
+ byMangled.set(mangleBorrowedFunction(fn.name), fn.name);
2234
+ }
2235
+ // Reverse mention edges, then propagate "may park" to every mentioner.
2236
+ const mentionedBy = new Map();
2237
+ const parks = new Set();
2238
+ const work = [];
2239
+ for (const [name, text] of this.fnDefText) {
2240
+ if (suspends.test(text)) {
2241
+ parks.add(name);
2242
+ work.push(name);
2243
+ }
2244
+ for (const match of text.matchAll(/@(sc_b?f_[A-Za-z0-9_]+)/g)) {
2245
+ const callee = byMangled.get(match[1]);
2246
+ if (callee === undefined || callee === name)
2247
+ continue;
2248
+ let callers = mentionedBy.get(callee);
2249
+ if (callers === undefined)
2250
+ mentionedBy.set(callee, (callers = new Set()));
2251
+ callers.add(name);
2252
+ }
2253
+ }
2254
+ while (work.length > 0) {
2255
+ const callers = mentionedBy.get(work.pop());
2256
+ if (callers === undefined)
2257
+ continue;
2258
+ for (const caller of callers) {
2259
+ if (parks.has(caller))
2260
+ continue;
2261
+ parks.add(caller);
2262
+ work.push(caller);
2263
+ }
2264
+ }
2265
+ for (const fn of this.mod.functions) {
2266
+ if (fn.async !== true || fn.generator !== undefined)
2267
+ continue;
2268
+ if (fn.asyncCacheGlobal !== undefined || fn.asyncCycleCacheGlobal !== undefined)
2269
+ continue;
2270
+ if (fn.returnType.kind === "dyn" || parks.has(fn.name))
2271
+ continue;
2272
+ if (!this.fnDefText.has(fn.name))
2273
+ continue;
2274
+ eligible.add(fn.name);
2275
+ }
2276
+ return eligible;
2277
+ }
2278
+ /** The spawn wrapper of a fiberless async function: runs the body on the
2279
+ * caller's stack between scr_async_inline_enter/leave and settles the
2280
+ * fresh promise from its outcome — the fiber trampoline's epilogue,
2281
+ * without the fiber. */
2282
+ emitFiberlessAsyncSpawn(fn, spawnParams) {
2283
+ const ret = fn.returnType;
2284
+ const retTy = this.llType(ret);
2285
+ const lifted = fn.captures !== undefined;
2286
+ this.declare(`declare void @scr_async_inline_enter(ptr)`);
2287
+ this.declare(`declare ptr @scr_async_inline_leave(ptr)`);
2288
+ this.declare(`declare void @scr_promise_reject_pending(ptr)`);
2289
+ if (lifted) {
2290
+ this.declare(`declare ptr @scr_closure_retain_v(ptr)`);
2291
+ this.declare(`declare void @scr_closure_release(ptr)`);
2292
+ }
2293
+ const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${spawnParams.join(", ")})`;
2294
+ const out = [
2295
+ `define internal ptr @${mangleAsyncSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; fiberless ${fn.name}`,
2296
+ `entry:`,
2297
+ ` %frame = alloca { ptr, ptr }`,
2298
+ // The fiber path holds +1 on a lifted environment for the body's
2299
+ // duration (packed, released by the trampoline); keep that lifetime.
2300
+ ...(lifted ? [` %env = call ptr @scr_closure_retain_v(ptr %a0)`] : []),
2301
+ ` call void @scr_async_inline_enter(ptr %frame)`,
2302
+ retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
2303
+ ...(lifted ? [` call void @scr_closure_release(ptr %a0)`] : []),
2304
+ ...this.pendingTestLines("%pend"),
2305
+ ` %p = call ptr @scr_async_inline_leave(ptr %frame)`,
2306
+ ` br i1 %pend, label %thrown, label %clean`,
2307
+ `clean:`,
2308
+ ...this.asyncFulfillLines(ret, "%p"),
2309
+ ` ret ptr %p`,
2310
+ `thrown:`,
2311
+ ];
2312
+ if (ret.kind !== "void" && isRefCounted(ret)) {
2313
+ // An escaping throw means %r is the never-read dummy (NULL).
2314
+ out.push(` call void ${releaseSym(this.shapeHost, ret)}(ptr %r)`);
2315
+ }
2316
+ out.push(` call void @scr_promise_reject_pending(ptr %p)`, ` ret ptr %p`, `}`, ``);
2317
+ return out;
2318
+ }
1818
2319
  /** Per-async-function machinery — async.ts's scaffolding, .ll
1819
2320
  * flavored: an argument-pack struct type, a fiber trampoline (unpacks,
1820
2321
  * frees the pack, runs the ordinary compiled body, settles the
@@ -1829,9 +2330,20 @@ export class LlEmitter {
1829
2330
  this.declare(`declare void @llvm.coro.destroy(ptr)`);
1830
2331
  out.push(`define void @scr_wasi_coro_resume(ptr %handle) ${FN_ATTRS} {`, `entry:`, ` call void @llvm.coro.resume(ptr %handle)`, ` ret void`, `}`, `define void @scr_wasi_coro_destroy(ptr %handle) ${FN_ATTRS} {`, `entry:`, ` call void @llvm.coro.destroy(ptr %handle)`, ` ret void`, `}`, ``);
1831
2332
  }
2333
+ const fiberless = this.fiberlessAsyncFunctions();
1832
2334
  for (const fn of this.mod.functions) {
1833
2335
  if (fn.async !== true || fn.generator !== undefined)
1834
2336
  continue;
2337
+ if (fiberless.has(fn.name)) {
2338
+ const fieldTys = [
2339
+ ...(fn.captures !== undefined ? ["ptr"] : []),
2340
+ ...fn.params.map((p) => this.llType(p.type)),
2341
+ ];
2342
+ const params = fieldTys.map((ty, i) => `${ty} %a${i}`);
2343
+ for (const line of this.emitFiberlessAsyncSpawn(fn, params))
2344
+ out.push(line);
2345
+ continue;
2346
+ }
1835
2347
  const { definitions, ret, tr, spawnParams, argPackLines } = this.emitArgPackAndTrampolinePrologue(fn);
1836
2348
  for (const line of definitions)
1837
2349
  out.push(line);
@@ -1848,35 +2360,9 @@ export class LlEmitter {
1848
2360
  if (fn.captures !== undefined) {
1849
2361
  this.declare(`declare void @scr_closure_release(ptr)`);
1850
2362
  }
1851
- tr.push(` %pend = call zeroext i1 @scr_exc_pending()`, ` br i1 %pend, label %thrown, label %clean`, `clean:`, ` %pr = call ptr @scr_fiber_promise(ptr %self)`);
1852
- switch (ret.kind) {
1853
- case "void":
1854
- this.declare(`declare void @scr_promise_fulfill_void(ptr)`);
1855
- tr.push(` call void @scr_promise_fulfill_void(ptr %pr)`);
1856
- break;
1857
- case "f64":
1858
- case "date":
1859
- this.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`);
1860
- tr.push(` call void @scr_promise_fulfill_f64(ptr %pr, double %r)`);
1861
- break;
1862
- case "bool":
1863
- this.declare(`declare void @scr_promise_fulfill_bool(ptr, i1 zeroext)`);
1864
- tr.push(` call void @scr_promise_fulfill_bool(ptr %pr, i1 %r)`);
1865
- break;
1866
- case "string":
1867
- this.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`);
1868
- tr.push(` call void @scr_promise_fulfill_str(ptr %pr, ptr %r) ; moves in`);
1869
- break;
1870
- case "dyn":
1871
- this.declare(`declare void @scr_promise_resolve_dyn(ptr, ptr)`);
1872
- tr.push(` call void @scr_promise_resolve_dyn(ptr %pr, ptr %r) ; moves in`);
1873
- break;
1874
- default: {
1875
- const v = vAdapters(this.shapeHost, ret);
1876
- this.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
1877
- tr.push(` call void @scr_promise_fulfill_ref(ptr %pr, ptr %r, ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(this.shapeHost, ret)})`);
1878
- }
1879
- }
2363
+ tr.push(...this.pendingTestLines("%pend"), ` br i1 %pend, label %thrown, label %clean`, `clean:`, ` %pr = call ptr @scr_fiber_promise(ptr %self)`);
2364
+ for (const line of this.asyncFulfillLines(ret, "%pr"))
2365
+ tr.push(line);
1880
2366
  tr.push(` ret void`, `thrown:`);
1881
2367
  if (ret.kind !== "void" && isRefCounted(ret)) {
1882
2368
  // An escaping throw means %r is the never-read dummy (NULL).
@@ -1983,7 +2469,7 @@ export class LlEmitter {
1983
2469
  tr.push(` ret void`, `}`, ``);
1984
2470
  }
1985
2471
  else {
1986
- tr.push(` %g = call ptr @scr_gen_of_fiber(ptr %self)`, ` %pend = call zeroext i1 @scr_exc_pending()`, ` br i1 %pend, label %thrown, label %clean`, `clean:`);
2472
+ tr.push(` %g = call ptr @scr_gen_of_fiber(ptr %self)`, ...this.pendingTestLines("%pend"), ` br i1 %pend, label %thrown, label %clean`, `clean:`);
1987
2473
  switch (ret.kind) {
1988
2474
  case "void":
1989
2475
  tr.push(` br label %done ; void body: the done value is undefined (NONE)`);
@@ -2073,10 +2559,30 @@ export class LlEmitter {
2073
2559
  return out;
2074
2560
  }
2075
2561
  // ── plumbing (frame and scope ownership) ──────
2562
+ /** A symbol derived from the constant's text rather than its first-use
2563
+ * index, so an edit that interns one more string leaves every other
2564
+ * constant's symbol, and the code referencing it, unchanged. This lets
2565
+ * native builds reuse the compiled partitions an edit does not touch. A
2566
+ * collision retries with a counter, still in first-use order. */
2567
+ contentSymbol(prefix, text) {
2568
+ for (let attempt = 0;; attempt++) {
2569
+ const digest = createHash("sha256")
2570
+ .update(attempt === 0 ? text : `${attempt}\0${text}`)
2571
+ .digest("hex");
2572
+ const symbol = `${prefix}${parseInt(digest.slice(0, 13), 16)}`;
2573
+ if (!this.contentSymbols.has(symbol)) {
2574
+ this.contentSymbols.add(symbol);
2575
+ return symbol;
2576
+ }
2577
+ }
2578
+ }
2076
2579
  internLiteral(text) {
2077
2580
  let lit = this.literals.get(text);
2078
2581
  if (!lit) {
2079
- lit = { sym: `sc_lit_${this.literals.size}`, len: Buffer.byteLength(text, "utf8") };
2582
+ lit = {
2583
+ sym: this.contentSymbol("sc_lit_", text),
2584
+ len: Buffer.byteLength(text, "utf8"),
2585
+ };
2080
2586
  this.literals.set(text, lit);
2081
2587
  }
2082
2588
  const name = `@${lit.sym}`;
@@ -2088,7 +2594,7 @@ export class LlEmitter {
2088
2594
  cstr(text) {
2089
2595
  let c = this.cstrs.get(text);
2090
2596
  if (!c) {
2091
- c = { sym: `sc_cs_${this.cstrs.size}`, len: Buffer.byteLength(text, "utf8") };
2597
+ c = { sym: this.contentSymbol("sc_cs_", text), len: Buffer.byteLength(text, "utf8") };
2092
2598
  this.cstrs.set(text, c);
2093
2599
  }
2094
2600
  return `@${c.sym}`;
@@ -2105,6 +2611,12 @@ export class LlEmitter {
2105
2611
  if (!arm || !isUnitType(arm)) {
2106
2612
  throw new InternalCompilerError(`llvm emitter bug: unit instance for non-unit arm ${tag} of ${unionId}`);
2107
2613
  }
2614
+ const nullable = this.nullableUnions.get(unionId);
2615
+ if (nullable) {
2616
+ // NULL is the unit arm of a nullable union (`undefined` when it has
2617
+ // both); the `null` sentinel is immortal. Neither owns anything.
2618
+ return tag === nullable.nullTag ? NULLABLE_NULL : "null";
2619
+ }
2108
2620
  const key = `${unionId}:${tag}`;
2109
2621
  let sym = this.unitInstances.get(key);
2110
2622
  if (!sym) {
@@ -2115,6 +2627,56 @@ export class LlEmitter {
2115
2627
  this.immortalValues.add(name);
2116
2628
  return name;
2117
2629
  }
2630
+ /** The immortal ABSENT state of an undefined-armed record field slot
2631
+ * (IR fieldAbsent). */
2632
+ absentInstanceRef(unionId) {
2633
+ if (undefinedArmTag({ kind: "union", unionId }, this.unionsById) < 0) {
2634
+ throw new InternalCompilerError(`llvm emitter bug: absent state of ${unionId} without undefined`);
2635
+ }
2636
+ if (this.nullableUnions.has(unionId)) {
2637
+ this.needsNullableAbsent = true;
2638
+ this.immortalValues.add(NULLABLE_ABSENT);
2639
+ return NULLABLE_ABSENT;
2640
+ }
2641
+ let sym = this.absentInstances.get(unionId);
2642
+ if (!sym) {
2643
+ sym = `sc_absent_${this.absentInstances.size}`;
2644
+ this.absentInstances.set(unionId, sym);
2645
+ }
2646
+ const name = `@${sym}`;
2647
+ this.immortalValues.add(name);
2648
+ return name;
2649
+ }
2650
+ /** i1: a union value loaded from a field slot is the ABSENT state — the
2651
+ * undefined arm's tag with the absent payload marker. */
2652
+ fieldAbsentTest(value, unionId) {
2653
+ return this.fieldAbsentTestIn(this.B, value, unionId);
2654
+ }
2655
+ /** fieldAbsentTest in another block builder (helper generators). A
2656
+ * nullable union's ABSENT state is the module's immortal sentinel. */
2657
+ fieldAbsentTestIn(B, value, unionId) {
2658
+ if (this.nullableUnions.has(unionId)) {
2659
+ this.needsNullableAbsent = true;
2660
+ const t = B.tmp();
2661
+ B.line(`${t} = icmp eq ptr ${value}, ${NULLABLE_ABSENT}`);
2662
+ return t;
2663
+ }
2664
+ return emitFieldAbsentTest(B, value, undefinedArmTag({ kind: "union", unionId }, this.unionsById));
2665
+ }
2666
+ /** Loads a record field slot as a VALUE: an undefined-armed union slot
2667
+ * holding the ABSENT state surfaces as the ordinary undefined arm, so
2668
+ * absence never travels beyond its slot (a later `{ f: r.f }` is an
2669
+ * explicit, present property). */
2670
+ loadRecordField(ptr, t) {
2671
+ const v = this.loadField(ptr, t);
2672
+ if (t.kind !== "union" || undefinedArmTag(t, this.unionsById) < 0)
2673
+ return v;
2674
+ const absent = this.fieldAbsentTest(v, t.unionId);
2675
+ const unit = this.unitInstanceRef(t.unionId, undefinedArmTag(t, this.unionsById));
2676
+ const out = this.B.tmp();
2677
+ this.B.line(`${out} = select i1 ${absent}, ptr ${unit}, ptr ${v}`);
2678
+ return out;
2679
+ }
2118
2680
  declare(decl) {
2119
2681
  this.decls.add(decl);
2120
2682
  }
@@ -2127,9 +2689,20 @@ export class LlEmitter {
2127
2689
  throw new InternalCompilerError("llvm emitter bug: no active statement frame");
2128
2690
  return frame;
2129
2691
  }
2692
+ /** Whether a value is immortal: an interned constant, or a temp marked by
2693
+ * markImmortal. */
2694
+ isImmortal(name) {
2695
+ return this.immortalValues.has(name) || this.B.immortalTemps.has(name);
2696
+ }
2697
+ /** Record that a temp holds an immortal value (a choice between interned
2698
+ * literals): nothing owns it, and retains, releases and moves skip it. */
2699
+ markImmortal(v) {
2700
+ this.B.immortalTemps.add(v.name);
2701
+ return v;
2702
+ }
2130
2703
  /** Registers an owned refcounted value on the current statement frame. */
2131
2704
  own(v) {
2132
- if (isRefCounted(v.type) && !this.immortalValues.has(v.name))
2705
+ if (isRefCounted(v.type) && !this.isImmortal(v.name))
2133
2706
  this.currentFrame().push(v);
2134
2707
  return v;
2135
2708
  }
@@ -2141,7 +2714,7 @@ export class LlEmitter {
2141
2714
  }
2142
2715
  /** Strike a refcounted temp from its frame: ownership is being moved. */
2143
2716
  moveTemp(v) {
2144
- if (!isRefCounted(v.type) || this.immortalValues.has(v.name))
2717
+ if (!isRefCounted(v.type) || this.isImmortal(v.name))
2145
2718
  return;
2146
2719
  for (let i = this.frames.length - 1; i >= 0; i--) {
2147
2720
  const idx = this.frames[i].findIndex((e) => e.name === v.name);
@@ -2155,7 +2728,7 @@ export class LlEmitter {
2155
2728
  /** The retained (+1) read of a refcounted value — type-directed through
2156
2729
  * the `_v` table (immortals skip, exactly the C retain calls). */
2157
2730
  retainValue(name, type) {
2158
- if (this.immortalValues.has(name))
2731
+ if (this.isImmortal(name))
2159
2732
  return name;
2160
2733
  const t = this.B.tmp();
2161
2734
  this.B.line(`${t} = call ptr ${retainSym(this.shapeHost, type)}(ptr ${name})`);
@@ -2164,7 +2737,7 @@ export class LlEmitter {
2164
2737
  /** The release call for one owned refcounted value — type-directed like
2165
2738
  * releaseCallC (all runtime releases are NULL-tolerant). */
2166
2739
  releaseValue(name, type) {
2167
- if (this.immortalValues.has(name))
2740
+ if (this.isImmortal(name))
2168
2741
  return;
2169
2742
  this.B.line(`call void ${releaseSym(this.shapeHost, type)}(ptr ${name})`);
2170
2743
  }
@@ -2174,8 +2747,7 @@ export class LlEmitter {
2174
2747
  const t = this.B.tmp();
2175
2748
  this.B.line(`${t} = load ptr, ptr ${v.name}`);
2176
2749
  if (v.boxed) {
2177
- this.declare(`declare void @scr_box_release(ptr)`);
2178
- this.B.line(`call void @scr_box_release(ptr ${t})`);
2750
+ this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`);
2179
2751
  }
2180
2752
  else {
2181
2753
  this.releaseValue(t, v.type);
@@ -2190,9 +2762,22 @@ export class LlEmitter {
2190
2762
  for (const e of scope) {
2191
2763
  const t = this.B.tmp();
2192
2764
  this.B.line(`${t} = load ptr, ptr ${e.slot}`);
2193
- if (e.boxed) {
2194
- this.declare(`declare void @scr_box_release(ptr)`);
2195
- this.B.line(`call void @scr_box_release(ptr ${t})`);
2765
+ if (e.boxed && e.lazy) {
2766
+ // Unreachable code after a terminator emits nothing; a new block
2767
+ // here would revive it.
2768
+ if (this.B.isTerminated())
2769
+ continue;
2770
+ const created = this.B.tmp();
2771
+ this.B.line(`${created} = icmp ne ptr ${t}, null`);
2772
+ const release = this.B.newLabel("lazy.release"), join = this.B.newLabel("lazy.release.j");
2773
+ this.B.condBr(created, release, join);
2774
+ this.B.startBlock(release);
2775
+ this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`);
2776
+ this.B.br(join);
2777
+ this.B.startBlock(join);
2778
+ }
2779
+ else if (e.boxed) {
2780
+ this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`);
2196
2781
  }
2197
2782
  else {
2198
2783
  this.releaseValue(t, e.type); // runtime releases are NULL-tolerant
@@ -2242,8 +2827,11 @@ export class LlEmitter {
2242
2827
  * function — and branch to the handler / return a dummy value (never
2243
2828
  * read: callers of a may-throw function test the pending flag before
2244
2829
  * using the result). Callers own the surrounding pending branch; a
2245
- * `throw` unwinds unconditionally. */
2246
- emitUnwind() {
2830
+ * `throw` unwinds unconditionally. `passThrough` names a scalar call
2831
+ * result of the function's own return type: with nothing to release, the
2832
+ * unwind returns it as the dummy, so a call in tail position keeps a single
2833
+ * return and stays eligible for tail-call and recursion elimination. */
2834
+ emitUnwind(passThrough) {
2247
2835
  if (this.mod.workers && this.tryStack.length > 0) {
2248
2836
  this.declare(`declare zeroext i1 @scr_context_stopping()`);
2249
2837
  const stopped = this.B.tmp();
@@ -2272,13 +2860,13 @@ export class LlEmitter {
2272
2860
  terminator = `br label %${this.currentWasiCoro.finalLabel}`;
2273
2861
  }
2274
2862
  else {
2275
- const t = this.currentReturnType;
2863
+ const t = this.llType(this.currentReturnType);
2276
2864
  terminator =
2277
- t.kind === "void"
2865
+ t === "void"
2278
2866
  ? "ret void"
2279
- : t.kind === "f64" || t.kind === "date"
2867
+ : t === "double"
2280
2868
  ? `ret double ${f64Lit(0)}`
2281
- : t.kind === "bool"
2869
+ : t === "i1"
2282
2870
  ? "ret i1 false"
2283
2871
  : "ret ptr null";
2284
2872
  }
@@ -2293,6 +2881,11 @@ export class LlEmitter {
2293
2881
  }
2294
2882
  }
2295
2883
  if (entries.length === 0) {
2884
+ if (passThrough !== undefined && !target && this.currentWasiCoro === null) {
2885
+ const t = this.llType(this.currentReturnType);
2886
+ if (t === "double" || t === "i1")
2887
+ terminator = `ret ${t} ${passThrough}`;
2888
+ }
2296
2889
  this.B.terminate(terminator);
2297
2890
  return;
2298
2891
  }
@@ -2303,24 +2896,147 @@ export class LlEmitter {
2303
2896
  }
2304
2897
  this.B.br(cleanup.label);
2305
2898
  }
2899
+ /** Instructions (two-space indented) that set the i1 `dest` to "an
2900
+ * exception is pending in the active context" — scr_exc_pending()'s
2901
+ * meaning — for the helper and adapter bodies that test it outside
2902
+ * emitPendingCheck. Executables read the runtime's exported active-cell
2903
+ * pointer and compare its i32 kind (offset 0) against SCR_EXC_NONE inline,
2904
+ * as emitPendingCheck does; the out-of-line call would be opaque to LLVM
2905
+ * because the runtime links as prebuilt objects. Libraries and worker
2906
+ * programs keep the runtime call, which is emitPendingCheck's path there
2907
+ * too (workers also observe cross-thread termination in it). */
2908
+ pendingTestLines(dest) {
2909
+ if (this.mod.lib !== undefined || this.mod.workers === true) {
2910
+ this.declare(`declare zeroext i1 @scr_exc_pending()`);
2911
+ return [` ${dest} = call zeroext i1 @scr_exc_pending()`];
2912
+ }
2913
+ this.declare(`@scr_exc_active = external global ptr`);
2914
+ return [
2915
+ ` ${dest}.cell = load ptr, ptr @scr_exc_active`,
2916
+ ` ${dest}.kind = load i32, ptr ${dest}.cell`,
2917
+ ` ${dest} = icmp ne i32 ${dest}.kind, 0`,
2918
+ ];
2919
+ }
2306
2920
  /** The emitter contract for exceptions: after EVERY call that can throw
2307
2921
  * (per the may-throw analysis), test the pending flag and unwind. The
2308
2922
  * call's result temp must join its frame BEFORE this runs so the unwind
2309
- * releases the dummy (NULL for refcounted kinds) harmlessly. */
2310
- emitPendingCheck() {
2923
+ * releases the dummy (NULL for refcounted kinds) harmlessly. A scalar
2924
+ * call result of the function's return type may be passed as the unwind's
2925
+ * dummy value (see emitUnwind). */
2926
+ emitPendingCheck(passThrough) {
2927
+ const through = passThrough !== undefined &&
2928
+ this.llType(passThrough.type) === this.llType(this.currentReturnType) &&
2929
+ (passThrough.type.kind === "f64" || passThrough.type.kind === "bool")
2930
+ ? passThrough.name
2931
+ : undefined;
2311
2932
  const B = this.B;
2312
2933
  if (B.isTerminated())
2313
2934
  return;
2314
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
2315
- const p = B.tmp();
2316
- B.line(`${p} = call zeroext i1 @scr_exc_pending()`);
2935
+ if (this.mod.lib !== undefined) {
2936
+ this.declare(`declare zeroext i1 @scr_exc_pending()`);
2937
+ const p = B.tmp();
2938
+ B.line(`${p} = call zeroext i1 @scr_exc_pending()`);
2939
+ const lu = B.newLabel("exc.u");
2940
+ const lk = B.newLabel("exc.k");
2941
+ B.condBr(p, lu, lk);
2942
+ B.startBlock(lu);
2943
+ this.emitUnwind(through);
2944
+ B.startBlock(lk);
2945
+ return;
2946
+ }
2947
+ // Executables test the active exception cell's kind inline (its first
2948
+ // field). Worker programs test the process-wide alert word instead: it
2949
+ // is nonzero while any script thread may have an exception pending in
2950
+ // its active cell or has been asked to stop, so the common case is one
2951
+ // ordinary global load (a thread-local access is a call on Darwin) and
2952
+ // the out-of-line scr_exc_pending answers for this thread, observing
2953
+ // cancellation and reinstalling the termination sentinel.
2954
+ this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
2955
+ let word;
2956
+ if (this.mod.workers === true) {
2957
+ this.declare(`@scr_exc_alert = external global i32, align 64`);
2958
+ word = B.tmp();
2959
+ B.line(`${word} = load atomic i32, ptr @scr_exc_alert monotonic, align 64`);
2960
+ }
2961
+ else {
2962
+ this.declare(`@scr_exc_active = external global ptr`);
2963
+ let cell;
2964
+ if (B.excCellInvariant) {
2965
+ // Fiber switches restore the active cell before control returns to a
2966
+ // synchronous frame, so its pointer is one value for the whole call:
2967
+ // load it once in the entry block and test only the kind here.
2968
+ if (B.excCell === null) {
2969
+ B.excCell = "%exc.cell";
2970
+ B.entryAllocas.push(`${B.excCell} = load ptr, ptr @scr_exc_active`);
2971
+ }
2972
+ cell = B.excCell;
2973
+ }
2974
+ else {
2975
+ cell = B.tmp();
2976
+ B.line(`${cell} = load ptr, ptr @scr_exc_active`);
2977
+ }
2978
+ word = B.tmp();
2979
+ B.line(`${word} = load i32, ptr ${cell}`);
2980
+ }
2981
+ const hit = B.tmp();
2982
+ const cold = B.tmp();
2983
+ B.line(`${hit} = icmp ne i32 ${word}, 0`);
2984
+ B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${hit}, i1 false)`);
2317
2985
  const lu = B.newLabel("exc.u");
2318
2986
  const lk = B.newLabel("exc.k");
2319
- B.condBr(p, lu, lk);
2987
+ if (this.mod.workers === true) {
2988
+ this.declare(`declare zeroext i1 @scr_exc_pending()`);
2989
+ const slow = B.newLabel("exc.s");
2990
+ B.condBr(cold, slow, lk);
2991
+ B.startBlock(slow);
2992
+ const p = B.tmp();
2993
+ B.line(`${p} = call zeroext i1 @scr_exc_pending()`);
2994
+ B.condBr(p, lu, lk);
2995
+ }
2996
+ else {
2997
+ B.condBr(cold, lu, lk);
2998
+ }
2320
2999
  B.startBlock(lu);
2321
- this.emitUnwind();
3000
+ this.emitUnwind(through);
2322
3001
  B.startBlock(lk);
2323
3002
  }
3003
+ /** After a runtime call that ALWAYS leaves an exception pending (the
3004
+ * compiler-resolved Node throws): the pending test would always answer
3005
+ * true, so unwind unconditionally. Emission continues in a block with no
3006
+ * predecessors so the caller's remaining instructions stay well-formed;
3007
+ * LLVM deletes it. */
3008
+ emitAlwaysPendingUnwind() {
3009
+ const B = this.B;
3010
+ if (B.isTerminated())
3011
+ return;
3012
+ this.emitUnwind();
3013
+ B.startBlock(B.newLabel("exc.dead"));
3014
+ }
3015
+ /** A recursive function's entry check: below the running stack's guard,
3016
+ * throw Node's RangeError and unwind like any other pending exception. */
3017
+ emitStackCheck() {
3018
+ const B = this.B;
3019
+ const tl = this.mod.workers === true ? "thread_local " : "";
3020
+ this.declare(`@scr_stack_guard = external ${tl}global ptr`);
3021
+ this.declare(`declare ptr @llvm.stacksave.p0()`);
3022
+ this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
3023
+ this.declare(`declare void @scr_stack_overflow()`);
3024
+ const sp = B.tmp();
3025
+ const guard = B.tmp();
3026
+ const low = B.tmp();
3027
+ const cold = B.tmp();
3028
+ B.line(`${sp} = call ptr @llvm.stacksave.p0()`);
3029
+ B.line(`${guard} = load ptr, ptr @scr_stack_guard`);
3030
+ B.line(`${low} = icmp ult ptr ${sp}, ${guard}`);
3031
+ B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${low}, i1 false)`);
3032
+ const over = B.newLabel("stack.over");
3033
+ const ok = B.newLabel("stack.ok");
3034
+ B.condBr(cold, over, ok);
3035
+ B.startBlock(over);
3036
+ B.line(`call void @scr_stack_overflow()`);
3037
+ this.emitUnwind();
3038
+ B.startBlock(ok);
3039
+ }
2324
3040
  workerLoopBudget() {
2325
3041
  if (!this.mod.workers)
2326
3042
  return null;
@@ -2418,26 +3134,27 @@ export class LlEmitter {
2418
3134
  const def = this.unionsById.get(v.type.unionId);
2419
3135
  if (!def)
2420
3136
  throw new InternalCompilerError(`llvm emitter bug: truthiness of unknown union ${v.type.unionId}`);
3137
+ const nullable = this.nullableUnions.get(def.id);
3138
+ // Object arms are always truthy: present iff neither unit encoding.
3139
+ if (nullable && isObjectArm(nullable))
3140
+ return emitNullablePresent(B, nullable, v.name);
3141
+ const unionId = def.id;
2421
3142
  const slot = B.slot();
2422
3143
  B.entryAllocas.push(`${slot} = alloca i1`);
2423
3144
  const join = B.newLabel("ut.j");
2424
3145
  this.unionTagSwitch(v.name, def, (arm) => {
2425
3146
  let valueName = "false";
2426
3147
  if (arm.kind === "f64" || arm.kind === "procStream") {
2427
- valueName = B.tmp();
2428
- this.declare(`declare double @scr_union_get_f64(ptr)`);
2429
- B.line(`${valueName} = call double @scr_union_get_f64(ptr ${v.name})`);
3148
+ valueName = this.unionGetF64(v.name);
2430
3149
  }
2431
3150
  else if (arm.kind === "bool") {
2432
- valueName = B.tmp();
2433
- this.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`);
2434
- B.line(`${valueName} = call zeroext i1 @scr_union_get_bool(ptr ${v.name})`);
3151
+ valueName = this.unionGetBool(v.name);
2435
3152
  }
2436
3153
  else if (arm.kind === "string") {
2437
- valueName = this.unionPeek(v.name);
3154
+ valueName = this.unionPeek(v.name, unionId);
2438
3155
  }
2439
3156
  else if (arm.kind === "bigint" || arm.kind === "dyn" || arm.kind === "jsval") {
2440
- valueName = this.unionPeek(v.name);
3157
+ valueName = this.unionPeek(v.name, unionId);
2441
3158
  }
2442
3159
  const truthy = this.truthyOf(arm.kind, valueName, true);
2443
3160
  B.line(`store i1 ${truthy}, ptr ${slot}`);
@@ -2543,22 +3260,22 @@ export class LlEmitter {
2543
3260
  }
2544
3261
  }
2545
3262
  // ── union plumbing ──────────────────────────────────────────────────────
2546
- /** Loads a union box's tag (i32). */
2547
- unionTag(uName) {
2548
- const p = this.B.tmp();
2549
- const t = this.B.tmp();
2550
- this.B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${uName}, i64 0, i32 1`);
2551
- this.B.line(`${t} = load i32, ptr ${p}`);
2552
- return t;
3263
+ /** A union value's tag (i32): the box's tag word, or the NULL test of a
3264
+ * nullable union's pointer. */
3265
+ unionTag(uName, unionId) {
3266
+ return emitUnionTag(this.B, this.nullableUnions.get(unionId), uName);
3267
+ }
3268
+ /** Guard for helpers that build or read a union's `%ScrUnion` box
3269
+ * directly (library-shaped unions with fixed arms): a nullable union
3270
+ * reaching one is an emitter bug, never a silent miscompile. */
3271
+ requireBoxedUnion(unionId) {
3272
+ if (this.nullableUnions.has(unionId))
3273
+ throw new InternalCompilerError(`llvm emitter bug: nullable union ${unionId} reached a tagged-box helper`);
2553
3274
  }
2554
3275
  /** The BORROWED payload pointer of a ref arm (scr_union_peek inlined —
2555
- * the runtime's is a static inline). */
2556
- unionPeek(uName) {
2557
- const p = this.B.tmp();
2558
- const t = this.B.tmp();
2559
- this.B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${uName}, i64 0, i32 5`);
2560
- this.B.line(`${t} = load ptr, ptr ${p}`);
2561
- return t;
3276
+ * the runtime's is a static inline); a nullable union is its payload. */
3277
+ unionPeek(uName, unionId) {
3278
+ return emitUnionPeek(this.B, this.nullableUnions.get(unionId), uName);
2562
3279
  }
2563
3280
  /** Emits `switch` over a union's tag with one block per arm; each arm
2564
3281
  * body must TERMINATE its block (the callers branch to a join). The
@@ -2566,7 +3283,7 @@ export class LlEmitter {
2566
3283
  * reads can group equivalent storage prefixes and emit one representative. */
2567
3284
  unionTagSwitch(uName, def, arm, fieldGroups) {
2568
3285
  const B = this.B;
2569
- const tag = this.unionTag(uName);
3286
+ const tag = this.unionTag(uName, def.id);
2570
3287
  const bad = B.newLabel("u.bad");
2571
3288
  const labels = [];
2572
3289
  if (fieldGroups) {
@@ -2610,29 +3327,46 @@ export class LlEmitter {
2610
3327
  B.terminate(`unreachable`);
2611
3328
  }
2612
3329
  /** The +1 extraction of a union's single narrowed arm (unionNarrow /
2613
- * the nullish-family reads): scalars via the runtime getters, ref arms
3330
+ * the nullish-family reads): scalars via inline payload loads, ref arms
2614
3331
  * a retained peek. */
2615
- unionExtract(uName, arm) {
2616
- const B = this.B;
2617
- if (arm.kind === "f64" || arm.kind === "procStream") {
2618
- const t = B.tmp();
2619
- this.declare(`declare double @scr_union_get_f64(ptr)`);
2620
- B.line(`${t} = call double @scr_union_get_f64(ptr ${uName})`);
2621
- return t;
2622
- }
2623
- if (arm.kind === "bool") {
2624
- const t = B.tmp();
2625
- this.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`);
2626
- B.line(`${t} = call zeroext i1 @scr_union_get_bool(ptr ${uName})`);
2627
- return t;
2628
- }
2629
- return this.retainValue(this.unionPeek(uName), arm);
3332
+ unionExtract(uName, unionId, arm) {
3333
+ if (arm.kind === "f64" || arm.kind === "procStream")
3334
+ return this.unionGetF64(uName);
3335
+ if (arm.kind === "bool")
3336
+ return this.unionGetBool(uName);
3337
+ return this.retainValue(this.unionPeek(uName, unionId), arm);
3338
+ }
3339
+ /** A scalar f64 arm's payload (scr_union_get_f64 inlined). Inline loads
3340
+ * keep stack boxes free of escaping calls, so LLVM can scalarize them. */
3341
+ unionGetF64(uName) {
3342
+ const p = this.B.tmp();
3343
+ const t = this.B.tmp();
3344
+ this.B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${uName}, i64 0, i32 5`);
3345
+ this.B.line(`${t} = load double, ptr ${p}`);
3346
+ return t;
3347
+ }
3348
+ /** A bool arm's payload (scr_union_get_bool inlined: nonzero slot). */
3349
+ unionGetBool(uName) {
3350
+ const p = this.B.tmp();
3351
+ const bits = this.B.tmp();
3352
+ const t = this.B.tmp();
3353
+ this.B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${uName}, i64 0, i32 5`);
3354
+ this.B.line(`${bits} = load i64, ptr ${p}`);
3355
+ this.B.line(`${t} = icmp ne i64 ${bits}, 0`);
3356
+ return t;
2630
3357
  }
2631
3358
  /** Constructs a union box around an OWNED (+1, already moved) value —
2632
3359
  * the scr_union_new_* dispatch of unionWrap and the wrap-into-join
2633
3360
  * sites (shift). */
2634
- unionNewOwned(tag, v) {
3361
+ unionNewOwned(unionId, tag, v) {
2635
3362
  const B = this.B;
3363
+ const nullable = this.nullableUnions.get(unionId);
3364
+ if (nullable) {
3365
+ // The owned payload IS the nullable union's value.
3366
+ if (tag !== nullable.refTag)
3367
+ throw new InternalCompilerError(`llvm emitter bug: payload wrap into unit arm ${tag} of ${unionId}`);
3368
+ return v.name;
3369
+ }
2636
3370
  const t = B.tmp();
2637
3371
  if (v.type.kind === "f64" || v.type.kind === "procStream") {
2638
3372
  this.declare(`declare ptr @scr_union_new_f64(i32, double)`);
@@ -2775,6 +3509,42 @@ export class LlEmitter {
2775
3509
  B.line(`${z} = zext i1 ${value} to i8`);
2776
3510
  B.line(`store i8 ${z}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
2777
3511
  }
3512
+ /** Reads an int32-specialized class field: the i32 slot is the value,
3513
+ * and its widening is the exact double every program observation sees. */
3514
+ loadInt32Field(ptr, type) {
3515
+ const B = this.B;
3516
+ const raw = B.tmp();
3517
+ const number = B.tmp();
3518
+ B.line(`${raw} = load i32, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
3519
+ B.line(`${number} = sitofp i32 ${raw} to double`);
3520
+ return {
3521
+ name: number,
3522
+ type,
3523
+ uint32: raw,
3524
+ integer: { name: raw, type: "i32", signed: true, range: INT32_RANGE },
3525
+ };
3526
+ }
3527
+ /** Stores a value the whole-program analysis proved to be an int32 (never
3528
+ * -0, NaN or fractional) into an int32-specialized class field. */
3529
+ storeInt32Field(ptr, value, expr) {
3530
+ if (!this.int32Slots.provenWrites.has(expr))
3531
+ throw new InternalCompilerError("llvm emitter bug: unproven int32 field write");
3532
+ const B = this.B;
3533
+ let bits;
3534
+ if (value.integer?.type === "i32")
3535
+ bits = value.integer.name;
3536
+ else if (value.integer) {
3537
+ bits = B.tmp();
3538
+ B.line(`${bits} = trunc i64 ${value.integer.name} to i32`);
3539
+ }
3540
+ else if (value.uint32 !== undefined)
3541
+ bits = value.uint32;
3542
+ else {
3543
+ bits = B.tmp();
3544
+ B.line(`${bits} = fptosi double ${value.name} to i32`);
3545
+ }
3546
+ B.line(`store i32 ${bits}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
3547
+ }
2778
3548
  // ── bindings ────────────────────────────────────────────────────────────
2779
3549
  /** A binding's storage: a module global, a plain local slot, or a boxed
2780
3550
  * local (the slot holds the capture BOX; access goes through it). */
@@ -2806,17 +3576,26 @@ export class LlEmitter {
2806
3576
  this.B.line(`${b} = load ptr, ptr ${slot}`);
2807
3577
  return b;
2808
3578
  }
3579
+ /** The payload slot of a capture box (ScrBox.slot, field 5). */
3580
+ boxSlot(box) {
3581
+ const slot = this.B.tmp();
3582
+ this.B.line(`${slot} = getelementptr inbounds %ScrBox, ptr ${box}, i32 0, i32 5`);
3583
+ return slot;
3584
+ }
2809
3585
  boxGet(box, t) {
2810
3586
  const B = this.B;
2811
3587
  const acc = boxAccess(t);
2812
3588
  const r = B.tmp();
3589
+ // Scalar payloads are read in place (scr_box_get_f64/bool are a
3590
+ // memcpy and a nonzero test of the same slot), so LLVM can keep a
3591
+ // captured number in a register across a loop of calls.
2813
3592
  if (acc === "f64") {
2814
- this.declare(`declare double @scr_box_get_f64(ptr)`);
2815
- B.line(`${r} = call double @scr_box_get_f64(ptr ${box})`);
3593
+ B.line(`${r} = load double, ptr ${this.boxSlot(box)}`);
2816
3594
  }
2817
3595
  else if (acc === "bool") {
2818
- this.declare(`declare zeroext i1 @scr_box_get_bool(ptr)`);
2819
- B.line(`${r} = call zeroext i1 @scr_box_get_bool(ptr ${box})`);
3596
+ const raw = B.tmp();
3597
+ B.line(`${raw} = load i64, ptr ${this.boxSlot(box)}`);
3598
+ B.line(`${r} = icmp ne i64 ${raw}, 0`);
2820
3599
  }
2821
3600
  else {
2822
3601
  this.declare(`declare ptr @scr_box_get_ref(ptr)`);
@@ -2829,18 +3608,45 @@ export class LlEmitter {
2829
3608
  const B = this.B;
2830
3609
  const acc = boxAccess(t);
2831
3610
  if (acc === "f64") {
2832
- this.declare(`declare void @scr_box_set_f64(ptr, double)`);
2833
- B.line(`call void @scr_box_set_f64(ptr ${box}, double ${value})`);
3611
+ B.line(`store double ${value}, ptr ${this.boxSlot(box)}`);
2834
3612
  }
2835
3613
  else if (acc === "bool") {
2836
- this.declare(`declare void @scr_box_set_bool(ptr, i1 zeroext)`);
2837
- B.line(`call void @scr_box_set_bool(ptr ${box}, i1 ${value})`);
3614
+ const raw = B.tmp();
3615
+ B.line(`${raw} = zext i1 ${value} to i64`);
3616
+ B.line(`store i64 ${raw}, ptr ${this.boxSlot(box)}`);
2838
3617
  }
2839
3618
  else {
2840
3619
  this.declare(`declare void @scr_box_set_ref(ptr, ptr)`);
2841
3620
  B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${value})`);
2842
3621
  }
2843
3622
  }
3623
+ /** The box a new environment captures for `localId` (borrowed). An
3624
+ * unchanged parameter gets one box on first capture, holding its own
3625
+ * reference to the value; the function scope releases the cached box. */
3626
+ captureBox(localId) {
3627
+ const cache = this.lazyCaptureBoxes.get(localId);
3628
+ if (cache === undefined)
3629
+ return this.loadBox(`%${mangleLocal(localId)}`);
3630
+ const B = this.B;
3631
+ const type = this.currentLocals.get(localId).type;
3632
+ const current = this.loadBox(cache);
3633
+ const missing = B.tmp();
3634
+ B.line(`${missing} = icmp eq ptr ${current}, null`);
3635
+ const create = B.newLabel("lazy.box"), join = B.newLabel("lazy.box.j");
3636
+ B.condBr(missing, create, join);
3637
+ B.startBlock(create);
3638
+ const stack = this.lazyStackBoxes.get(localId);
3639
+ const box = stack ?? B.tmp();
3640
+ if (!stack)
3641
+ B.line(`${box} = ${boxNewCall(this.shapeHost, type)}`);
3642
+ const value = B.tmp();
3643
+ B.line(`${value} = load ${this.llType(type)}, ptr %${mangleLocal(localId)}`);
3644
+ this.boxSet(box, type, isRefCounted(type) ? this.retainValue(value, type) : value);
3645
+ B.line(`store ptr ${box}, ptr ${cache}`);
3646
+ B.br(join);
3647
+ B.startBlock(join);
3648
+ return this.loadBox(cache);
3649
+ }
2844
3650
  retainBox(box) {
2845
3651
  this.needsRetainBox = true;
2846
3652
  const t = this.B.tmp();
@@ -2885,6 +3691,12 @@ export class LlEmitter {
2885
3691
  B.line(`${empty} = icmp eq ptr ${value}, null`);
2886
3692
  this.throwIfUninitialized(empty, name);
2887
3693
  }
3694
+ /** Throws the TDZ ReferenceError for `name` unless the i1 `flag` is set. */
3695
+ checkInitializedFlag(flag, name) {
3696
+ const empty = this.B.tmp();
3697
+ this.B.line(`${empty} = xor i1 ${flag}, true`);
3698
+ this.throwIfUninitialized(empty, name);
3699
+ }
2888
3700
  throwIfUninitialized(empty, name) {
2889
3701
  const B = this.B;
2890
3702
  const lt = B.newLabel("tdz.t");
@@ -3046,6 +3858,9 @@ export class LlEmitter {
3046
3858
  emitFunction(fn) {
3047
3859
  const B = new BlockBuilder();
3048
3860
  this.B = B;
3861
+ // Fiber switches restore the active exception cell before control
3862
+ // returns to a synchronous frame (emitPendingCheck).
3863
+ B.excCellInvariant = fn.async !== true && fn.generator === undefined;
3049
3864
  this.debugScope = this.debug?.function(fn) ?? null;
3050
3865
  B.debugLocation = this.debug?.location(fn.loc, this.debugScope) ?? null;
3051
3866
  this.frames = [];
@@ -3053,13 +3868,23 @@ export class LlEmitter {
3053
3868
  this.unwindCleanups.clear();
3054
3869
  this.jumpTargets = [];
3055
3870
  this.currentLocals = new Map(fn.locals.map((l) => [l.id, l]));
3871
+ this.lazyCaptureBoxes.clear();
3872
+ this.lazyStackBoxes.clear();
3873
+ // An unchanged captured parameter keeps its value in a plain slot; the
3874
+ // binding is boxed only for closures (captureBox).
3875
+ const lazyParameters = this.lazyCaptures.parameters(fn);
3876
+ for (const id of lazyParameters) {
3877
+ const { boxed: _boxed, ...plain } = this.currentLocals.get(id);
3878
+ this.currentLocals.set(id, plain);
3879
+ this.lazyCaptureBoxes.set(id, "");
3880
+ }
3056
3881
  this.currentConstantCallbacks =
3057
3882
  this.debug === null ? (this.constantCallbacks.get(fn.name) ?? new Map()) : new Map();
3058
3883
  // Preserve concrete source bindings for debugger inspection. Suspended
3059
3884
  // functions keep the established array lifetime across continuations.
3060
3885
  this.streamingSplitsEnabled = this.debug === null && !fn.async && !fn.generator;
3061
3886
  this.stackCallbacks.reset(this.streamingSplitsEnabled);
3062
- this.stackCaptures.reset(fn, this.streamingSplitsEnabled);
3887
+ this.stackCaptures.reset(fn, this.streamingSplitsEnabled, lazyParameters);
3063
3888
  this.privateSplitLocals = this.streamingSplitsEnabled ? findPrivateSplitLocals(fn) : new Map();
3064
3889
  this.storedSplits.clear();
3065
3890
  this.splitSpans.clear();
@@ -3068,11 +3893,21 @@ export class LlEmitter {
3068
3893
  const numericFn = withInitializerBindings(fn, initializerBindings);
3069
3894
  this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
3070
3895
  this.borrowedParameters.clear();
3896
+ this.reboundOwnerSlots.clear();
3897
+ this.projectedParameters.clear();
3898
+ const projectedParameterIndexes = this.callLifetimes.parameters.get(fn.name);
3899
+ if (projectedParameterIndexes)
3900
+ for (const index of projectedParameterIndexes)
3901
+ this.projectedParameters.add(fn.params[index].localId);
3071
3902
  this.stableCallBindings = this.callLifetimes.bindings.get(fn.name) ?? new Set();
3072
3903
  const borrowedParameterIndexes = this.callLifetimes.borrowed.get(fn.name);
3073
3904
  if (borrowedParameterIndexes) {
3905
+ // Rebound walk parameters borrow too, but only unchanged ones may
3906
+ // serve as stable owners for call arguments and aliases.
3074
3907
  for (const index of borrowedParameterIndexes)
3075
- this.borrowedParameters.add(fn.params[index].localId);
3908
+ if (!this.walkBorrowsByFunction.get(fn.name)?.has(fn.params[index].localId) &&
3909
+ !this.reboundByFunction.get(fn.name)?.has(fn.params[index].localId))
3910
+ this.borrowedParameters.add(fn.params[index].localId);
3076
3911
  }
3077
3912
  this.captureIds = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId));
3078
3913
  this.integerLoopBindings.clear();
@@ -3093,17 +3928,19 @@ export class LlEmitter {
3093
3928
  this.callArrayReads = findCallArrayReads(fn, this.optionalArrayReads, this.referenceEffects.functions, this.callLifetimes);
3094
3929
  this.scalarStringSlices =
3095
3930
  this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
3096
- this.mapReadLifetimes = findMapReadLifetimes(fn, this.unionsById, this.callLifetimes);
3097
- this.localStackUnions = findLocalStackUnions(fn, this.callLifetimes, this.unionsById);
3098
- this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.unionsById);
3931
+ this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
3932
+ this.localStackUnions = findLocalStackUnions(fn, this.callLifetimes, this.boxedUnionsById);
3933
+ this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.boxedUnionsById, (e) => this.nullableFieldGet(e) !== null);
3099
3934
  this.localUnionStorage = new Map();
3100
- this.integerRanges = analyzeIntegerRanges(numericFn);
3935
+ this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set();
3936
+ this.integerRanges = analyzeIntegerRanges(numericFn, this.int32Slots.facts(fn.name));
3101
3937
  this.bytesBounds = findBytesBounds(numericFn, this.integerRanges);
3102
3938
  this.chainSlots.clear();
3103
3939
  this.finallyStack = [];
3104
3940
  this.tryStack = [];
3105
3941
  this.currentReturnType = fn.returnType;
3106
3942
  this.currentGenerator = fn.generator ?? null;
3943
+ this.currentBorrowedReturn = this.borrowedReturns.has(fn.name);
3107
3944
  this.currentWasiCoro = null;
3108
3945
  this.logArgSlots = 0;
3109
3946
  if (this.wasi && (fn.async === true || fn.generator !== undefined)) {
@@ -3154,7 +3991,7 @@ export class LlEmitter {
3154
3991
  B.line(`${value} = load double, ptr @${mangleGlobal(global.id)}`);
3155
3992
  this.storeIntegerView(global.id, { name: value, type: global.type });
3156
3993
  }
3157
- for (const local of fn.locals) {
3994
+ for (const local of this.currentLocals.values()) {
3158
3995
  // Boxed locals' slots hold their capture BOX (a ptr); captured
3159
3996
  // (env-borrowed) locals bind the incoming box below. A caught-typed
3160
3997
  // local is a catch binding: its slot holds the ScrCaught snapshot
@@ -3203,6 +4040,26 @@ export class LlEmitter {
3203
4040
  // params (callees own their params — callers passed +1). Boxed params
3204
4041
  // allocate the shared binding and move the raw value in.
3205
4042
  const fnScope = [];
4043
+ for (const id of this.lazyCaptureBoxes.keys()) {
4044
+ const cache = B.slot();
4045
+ B.entryAllocas.push(`${cache} = alloca ptr ; lazy box ${this.currentLocals.get(id).name}`);
4046
+ B.line(`store ptr null, ptr ${cache}`);
4047
+ fnScope.push({
4048
+ slot: cache,
4049
+ type: this.currentLocals.get(id).type,
4050
+ boxed: true,
4051
+ lazy: true,
4052
+ });
4053
+ this.lazyCaptureBoxes.set(id, cache);
4054
+ // Environments that finish during the call can share an immortal
4055
+ // frame box. Its payload owner is released with the function scope.
4056
+ const stack = this.stackCaptures.emit(this, id, this.currentLocals.get(id).type);
4057
+ if (stack) {
4058
+ this.lazyStackBoxes.set(id, stack.box);
4059
+ if (stack.owner)
4060
+ fnScope.push(stack.owner);
4061
+ }
4062
+ }
3206
4063
  const borrowed = this.callLifetimes.borrowed.get(fn.name);
3207
4064
  for (const [index, p] of fn.params.entries()) {
3208
4065
  const local = this.currentLocals.get(p.localId);
@@ -3219,6 +4076,15 @@ export class LlEmitter {
3219
4076
  this.storeIntegerView(p.localId, { name: `%p_${mangleLocal(p.localId)}`, type: p.type });
3220
4077
  if (isRefCounted(p.type) && !borrowed?.has(index))
3221
4078
  fnScope.push({ slot, type: p.type });
4079
+ if (this.reboundByFunction.get(fn.name)?.has(p.localId) && borrowed?.has(index)) {
4080
+ // The caller's borrow stays in the parameter slot; values the body
4081
+ // assigns are owned here until rebinding or exit releases them.
4082
+ const owner = `${slot}.owner`;
4083
+ B.entryAllocas.push(`${owner} = alloca ptr`);
4084
+ B.line(`store ptr null, ptr ${owner}`);
4085
+ fnScope.push({ slot: owner, type: p.type });
4086
+ this.reboundOwnerSlots.set(p.localId, owner);
4087
+ }
3222
4088
  }
3223
4089
  this.scopes.push(fnScope);
3224
4090
  // Stackful native fibers retain these frames while suspended. The
@@ -3231,7 +4097,9 @@ export class LlEmitter {
3231
4097
  B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${this.cstr(frame)})`);
3232
4098
  B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
3233
4099
  }
3234
- if (this.mod.workers)
4100
+ if (this.stackChecks?.has(fn.name))
4101
+ this.emitStackCheck();
4102
+ if (this.workerEntryPolls?.has(fn.name))
3235
4103
  this.emitPendingCheck();
3236
4104
  this.emitStmts(fn.body);
3237
4105
  // Implicit exit of a void function: release the function scope unless
@@ -3303,24 +4171,55 @@ export class LlEmitter {
3303
4171
  const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`;
3304
4172
  const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
3305
4173
  const body = `define internal ${ret} @${symbol}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
3306
- return borrowed ? body + "\n" + this.emitOwnedCallAdapter(fn, borrowed) : body;
4174
+ if (!borrowed)
4175
+ return body;
4176
+ const slot = this.implSlotBorrowed(fn.name);
4177
+ const virtual = slot.size > 0 && slot.size !== borrowed.size
4178
+ ? "\n" + this.emitOwnedCallAdapter(fn, borrowed, slot)
4179
+ : "";
4180
+ return body + "\n" + this.emitOwnedCallAdapter(fn, borrowed) + virtual;
3307
4181
  }
3308
4182
  /** Keep the ordinary owned ABI for closures, virtual dispatch, generated
3309
4183
  * runtime adapters and library exports. Only direct IR calls select the
3310
4184
  * borrowing body. Ownership of other parameters moves into that body;
3311
4185
  * borrowed parameters are released here on normal and exceptional exits.
3312
4186
  * The pending exception remains for the adapter's caller to handle. */
3313
- emitOwnedCallAdapter(fn, borrowed) {
4187
+ /** With `kept`, the virtual adapter of a borrowing slot instead: the
4188
+ * slot's parameters stay borrowed, the body's other borrowed ones are
4189
+ * released here. */
4190
+ emitOwnedCallAdapter(fn, borrowed, kept = NO_BORROWED) {
3314
4191
  const B = new BlockBuilder();
3315
4192
  this.B = B;
3316
4193
  const params = fn.params.map((param, index) => `${this.llType(param.type)} %p${index}`);
3317
4194
  const ret = this.llType(fn.returnType);
3318
4195
  const call = `call ${ret} @${mangleBorrowedFunction(fn.name)}(${params.join(", ")})`;
3319
4196
  B.line(ret === "void" ? call : `%result = ${call}`);
4197
+ // A borrowed result is reachable from the parameters: own it first. A
4198
+ // throwing body returns a null dummy, which needs no owner (and string
4199
+ // or array retains do not accept NULL).
4200
+ let result = "%result";
4201
+ if (this.borrowedReturns.has(fn.name)) {
4202
+ const slot = B.slot();
4203
+ B.entryAllocas.push(`${slot} = alloca ptr`);
4204
+ B.line(`store ptr null, ptr ${slot}`);
4205
+ const present = B.tmp();
4206
+ const own = B.newLabel("result.own");
4207
+ const done = B.newLabel("result.done");
4208
+ B.line(`${present} = icmp ne ptr %result, null`);
4209
+ B.condBr(present, own, done);
4210
+ B.startBlock(own);
4211
+ B.line(`store ptr ${this.retainValue("%result", fn.returnType)}, ptr ${slot}`);
4212
+ B.br(done);
4213
+ B.startBlock(done);
4214
+ result = B.tmp();
4215
+ B.line(`${result} = load ptr, ptr ${slot}`);
4216
+ }
3320
4217
  for (const index of borrowed)
3321
- this.releaseValue(`%p${index}`, fn.params[index].type);
3322
- B.terminate(ret === "void" ? "ret void" : `ret ${ret} %result`);
3323
- return `define internal ${ret} @${mangleFunction(fn.name)}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`;
4218
+ if (!kept.has(index))
4219
+ this.releaseValue(`%p${index}`, fn.params[index].type);
4220
+ B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`);
4221
+ const symbol = kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name);
4222
+ return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`;
3324
4223
  }
3325
4224
  /** A local stable binding keeps its value alive while later arguments
3326
4225
  * and the callee execute. A proven local union box cannot escape or be
@@ -3329,6 +4228,33 @@ export class LlEmitter {
3329
4228
  canBorrowCallArgument(value) {
3330
4229
  if (value.kind === "strLit")
3331
4230
  return true;
4231
+ // Class casts reinterpret the same pointer (prefix layouts).
4232
+ if ((value.kind === "upcast" || value.kind === "downcast") &&
4233
+ value.value.type.kind === "object")
4234
+ return this.canBorrowCallArgument(value.value);
4235
+ if (value.kind === "seqExpr") {
4236
+ const guarded = guardedValue(value);
4237
+ return guarded !== null && this.canBorrowCallArgument(guarded);
4238
+ }
4239
+ // A nullable-union wrap reinterprets its payload pointer; an immortal
4240
+ // unit constant needs no owner at all.
4241
+ const wrap = this.borrowableNullableWrap(value);
4242
+ if (wrap === "unit")
4243
+ return true;
4244
+ if (wrap === "ref" && value.kind === "unionWrap")
4245
+ return this.canBorrowCallArgument(value.value);
4246
+ // Checked casts (`x as C`, `x!` on a nullable) lower to a ternary whose
4247
+ // failing arm always throws; the successful arm projects the same
4248
+ // stable owner. The throwing arm's typed dummy owns nothing.
4249
+ if (value.kind === "unionNarrow")
4250
+ return this.canBorrowCallArgument(value.value);
4251
+ if (value.kind === "libCall")
4252
+ return value.fn === "error.nodeThrow";
4253
+ if (value.kind === "ternary")
4254
+ return (this.canBorrowReceiver(value.then) &&
4255
+ this.canBorrowReceiver(value.else_) &&
4256
+ this.canBorrowCallArgument(value.then) &&
4257
+ this.canBorrowCallArgument(value.else_));
3332
4258
  if (value.kind !== "varRef")
3333
4259
  return false;
3334
4260
  const binding = this.binding(value.localId);
@@ -3383,6 +4309,12 @@ export class LlEmitter {
3383
4309
  switch (s.kind) {
3384
4310
  case "varDecl": {
3385
4311
  const b = this.binding(s.localId);
4312
+ if (this.walkBorrows.has(s.localId) && b.kind === "local") {
4313
+ // A borrowed walk pointer: no retain now, no release at scope exit.
4314
+ const v = s.init === null ? "null" : this.emitReadReceiver(s.init).name;
4315
+ B.line(`store ptr ${v}, ptr ${b.slot}`);
4316
+ break;
4317
+ }
3386
4318
  const slice = this.scalarStringSlices.get(s.localId);
3387
4319
  if (slice) {
3388
4320
  const snapshot = emitStringSliceSnapshot(this, slice);
@@ -3515,6 +4447,23 @@ export class LlEmitter {
3515
4447
  break;
3516
4448
  }
3517
4449
  case "assign": {
4450
+ const owner = this.reboundOwnerSlots.get(s.localId);
4451
+ if (owner !== undefined && !this.walkBorrows.has(s.localId)) {
4452
+ // A rebound borrowed parameter: the new value moves into the owner
4453
+ // slot, replacing (and releasing) the previous owned value. This
4454
+ // precedes the in-place string append, which would release the
4455
+ // caller's borrow.
4456
+ const param = this.binding(s.localId);
4457
+ const slot = param.slot;
4458
+ const v = this.emitExpr(s.value);
4459
+ this.moveTemp(v);
4460
+ const old = B.tmp();
4461
+ B.line(`${old} = load ptr, ptr ${owner}`);
4462
+ this.releaseValue(old, param.type);
4463
+ B.line(`store ptr ${v.name}, ptr ${slot}`);
4464
+ B.line(`store ptr ${v.name}, ptr ${owner}`);
4465
+ break;
4466
+ }
3518
4467
  const localStorage = this.localUnionStorage.get(s.localId);
3519
4468
  if (localStorage) {
3520
4469
  storeLocalUnion(this, localStorage, s.value);
@@ -3527,6 +4476,11 @@ export class LlEmitter {
3527
4476
  break;
3528
4477
  }
3529
4478
  const b = this.binding(s.localId);
4479
+ if (this.walkBorrows.has(s.localId) && b.kind === "local") {
4480
+ // Rebinding a borrowed walk pointer releases nothing.
4481
+ B.line(`store ptr ${this.emitReadReceiver(s.value).name}, ptr ${b.slot}`);
4482
+ break;
4483
+ }
3530
4484
  const v = this.emitExpr(s.value);
3531
4485
  if (b.kind === "global" && !s.initializes)
3532
4486
  this.checkGlobalTdz(s.localId);
@@ -3548,7 +4502,9 @@ export class LlEmitter {
3548
4502
  break;
3549
4503
  }
3550
4504
  case "exprStmt":
3551
- this.emitExpr(s.expr);
4505
+ // A statement-position splice never observes its removed elements.
4506
+ if (!emitDiscardedSplice(this, s.expr))
4507
+ this.emitDiscarded(s.expr);
3552
4508
  break;
3553
4509
  case "arraySet": {
3554
4510
  // Evaluation order matches JS: array, index, then value. Ownership
@@ -3560,21 +4516,43 @@ export class LlEmitter {
3560
4516
  if (s.arr.type.kind !== "array")
3561
4517
  throw new InternalCompilerError("llvm emitter bug: arraySet on non-array");
3562
4518
  const acc = elemAccess(s.arr.type.elem);
4519
+ this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
3563
4520
  if (acc === "ref")
3564
4521
  this.moveTemp(v);
3565
- const argTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
3566
- this.declare(`declare void @scr_arr_set_${acc}(ptr, double, ${argTy === "i1" ? "i1 zeroext" : argTy})`);
3567
- B.line(`call void @scr_arr_set_${acc}(ptr ${arr.name}, double ${idx.name}, ${argTy} ${v.name})`);
4522
+ emitDenseArraySet(this, arr.name, idx, s.index, acc, s.arr.type.elem, v.name);
3568
4523
  break;
3569
4524
  }
3570
4525
  case "arraySetLength": {
4526
+ // Pop's `length = 0` store on an empty array only matters when the
4527
+ // array can be frozen (published); its operand is a local.
4528
+ if (s.pop === true &&
4529
+ s.length.kind === "numLit" &&
4530
+ s.length.value === 0 &&
4531
+ this.publishedTypes?.containerGuarded(s.arr.type) !== true)
4532
+ break;
3571
4533
  const arr = emitBorrowedInput(this, s.arr);
3572
4534
  const length = this.emitExpr(s.length);
3573
4535
  if (s.arr.type.kind !== "array")
3574
4536
  throw new InternalCompilerError("llvm emitter bug: arraySetLength on non-array");
4537
+ this.emitPublishedArrayGuard(arr.name, s.arr.type, s.pop === true ? 2 : 3);
3575
4538
  this.declare(`declare void @scr_arr_set_len(ptr, double)`);
4539
+ // Assigning the current length (a reused work array reset to where
4540
+ // it already is) changes nothing: the runtime's validation accepts
4541
+ // exactly the value equal to the length (-0 included, NaN never),
4542
+ // and no element is released. Everything else keeps the call.
4543
+ const lenPtr = B.tmp(), len = B.tmp(), lenNumber = B.tmp(), same = B.tmp();
4544
+ const resize = B.newLabel("arr.len.set"), done = B.newLabel("arr.len.done");
4545
+ B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
4546
+ this.markMemoryPointer(lenPtr, "array:header");
4547
+ B.line(`${len} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`);
4548
+ B.line(`${lenNumber} = uitofp ${this.sizeType} ${len} to double`);
4549
+ B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`);
4550
+ B.condBr(same, done, resize);
4551
+ B.startBlock(resize);
3576
4552
  B.line(`call void @scr_arr_set_len(ptr ${arr.name}, double ${length.name})`);
3577
4553
  this.emitPendingCheck();
4554
+ B.br(done);
4555
+ B.startBlock(done);
3578
4556
  break;
3579
4557
  }
3580
4558
  case "arraySetUndefined":
@@ -3583,6 +4561,7 @@ export class LlEmitter {
3583
4561
  const idx = this.emitExpr(s.index);
3584
4562
  if (s.arr.type.kind !== "array")
3585
4563
  throw new InternalCompilerError(`llvm emitter bug: ${s.kind} on non-array`);
4564
+ this.emitPublishedArrayGuard(arr.name, s.arr.type, s.kind === "arrayDelete" ? 5 : 0, idx.name);
3586
4565
  const fn = s.kind === "arraySetUndefined" ? "scr_arr_set_undefined" : "scr_arr_delete";
3587
4566
  this.declare(`declare ${s.kind === "arrayDelete" ? "zeroext i1" : "void"} @${fn}(ptr, double)`);
3588
4567
  if (s.kind === "arrayDelete")
@@ -3612,10 +4591,29 @@ export class LlEmitter {
3612
4591
  // release — a release can trigger a cycle collection, which must
3613
4592
  // never see a heap edge whose count was already given up).
3614
4593
  // Classes and records share the struct layout, so one emission.
3615
- const obj = isRefCounted(s.value.type)
3616
- ? this.emitExpr(s.obj)
3617
- : this.emitStableReceiver(s.obj, [s.value]);
4594
+ const nullable = s.kind === "fieldSet" ? this.nullableFields.get(s.className, s.field) : null;
4595
+ if (nullable && s.kind === "fieldSet") {
4596
+ const obj = this.emitStableReceiver(s.obj, [s.value]);
4597
+ // (The guard precedes the value here: the nullable store
4598
+ // evaluates it itself.)
4599
+ this.emitPublishedFieldGuard(obj.name, s.className, s.field);
4600
+ this.emitNullableFieldStore(obj.name, s.className, s.field, nullable, s.value);
4601
+ break;
4602
+ }
4603
+ if (s.kind === "fieldSet" && this.int32Slots.isField(s.className, s.field)) {
4604
+ const obj = this.emitStableReceiver(s.obj, [s.value]);
4605
+ const v = this.emitExpr(s.value);
4606
+ this.emitPublishedFieldGuard(obj.name, s.className, s.field);
4607
+ const { ptr } = this.classFieldPtr(obj.name, s.className, s.field);
4608
+ this.storeInt32Field(ptr, v, s.value);
4609
+ break;
4610
+ }
4611
+ const obj = this.emitStableReceiver(s.obj, [s.value]);
3618
4612
  const v = this.emitExpr(s.value);
4613
+ if (s.kind === "fieldSet")
4614
+ this.emitPublishedFieldGuard(obj.name, s.className, s.field);
4615
+ else if (this.publishedTypes?.recordGuarded(s.shapeId) === true)
4616
+ this.emitPublishedFieldGuard(obj.name, null, s.field);
3619
4617
  const { ptr, type } = s.kind === "fieldSet"
3620
4618
  ? this.classFieldPtr(obj.name, s.className, s.field)
3621
4619
  : this.recordFieldPtr(obj.name, s.shapeId, s.field);
@@ -3649,6 +4647,11 @@ export class LlEmitter {
3649
4647
  // the overflow (key and value released; absent keys no-op).
3650
4648
  const obj = this.emitExpr(s.obj);
3651
4649
  const key = this.emitExpr(s.key);
4650
+ if (this.publishedTypes?.recordGuarded(s.shapeId) === true) {
4651
+ // Node: "Cannot delete property 'k' of #<Object>" (key known at
4652
+ // run time only; the guard reports the frozen record generically).
4653
+ this.emitPublishedFieldGuard(obj.name, null, "[key]");
4654
+ }
3652
4655
  const ovf = this.recordOvfPtr(obj.name, s.shapeId);
3653
4656
  this.declare(`declare zeroext i1 @scr_map_delete_str(ptr, ptr)`);
3654
4657
  const t = B.tmp();
@@ -3664,6 +4667,8 @@ export class LlEmitter {
3664
4667
  const obj = this.emitExpr(s.obj);
3665
4668
  const key = this.emitExpr(s.key);
3666
4669
  const v = this.emitExpr(s.value);
4670
+ if (this.publishedTypes?.recordGuarded(s.shapeId) === true)
4671
+ this.emitPublishedFieldGuard(obj.name, null, "[key]");
3667
4672
  if (isRefCounted(v.type))
3668
4673
  this.moveTemp(v);
3669
4674
  const shape = this.recordShape(s.shapeId);
@@ -3695,7 +4700,7 @@ export class LlEmitter {
3695
4700
  for (const f of shape.fields) {
3696
4701
  const lit = this.internLiteral(f.name);
3697
4702
  const hit = B.tmp();
3698
- B.line(`${hit} = call zeroext i1 @scr_str_eq(ptr ${key.name}, ptr ${lit}) ; ${llvmCommentText(f.name)}`);
4703
+ B.line(`${hit} = call zeroext i1 @sc_str_eq(ptr ${key.name}, ptr ${lit}) ; ${llvmCommentText(f.name)}`);
3699
4704
  const lh = B.newLabel("rks.h");
3700
4705
  const ln = B.newLabel("rks.n");
3701
4706
  B.condBr(hit, lh, ln);
@@ -3718,9 +4723,9 @@ export class LlEmitter {
3718
4723
  B.line(`call void @scr_dyn_release(ptr ${v.name})`);
3719
4724
  // Mismatched write: TypeError pending, field untouched — the
3720
4725
  // statement-level check below unwinds.
3721
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
3722
4726
  const pend = B.tmp();
3723
- B.line(`${pend} = call zeroext i1 @scr_exc_pending()`);
4727
+ for (const line of this.pendingTestLines(pend))
4728
+ B.line(line.trimStart());
3724
4729
  const lw = B.newLabel("rks.w");
3725
4730
  B.condBr(pend, join, lw);
3726
4731
  B.startBlock(lw);
@@ -3751,7 +4756,7 @@ export class LlEmitter {
3751
4756
  // typeEquals(f.type, iv) — the frontend fences everything else.
3752
4757
  const lit = this.internLiteral(f.name);
3753
4758
  const hit = B.tmp();
3754
- B.line(`${hit} = call zeroext i1 @scr_str_eq(ptr ${key.name}, ptr ${lit}) ; ${llvmCommentText(f.name)}`);
4759
+ B.line(`${hit} = call zeroext i1 @sc_str_eq(ptr ${key.name}, ptr ${lit}) ; ${llvmCommentText(f.name)}`);
3755
4760
  const lh = B.newLabel("rks.h");
3756
4761
  const ln = B.newLabel("rks.n");
3757
4762
  B.condBr(hit, lh, ln);
@@ -4125,10 +5130,13 @@ export class LlEmitter {
4125
5130
  B.line(`${inBounds} = icmp ne ptr ${cur}, null`);
4126
5131
  }
4127
5132
  else {
4128
- const i = B.tmp(), len = B.tmp();
4129
- this.declare(`declare double @scr_arr_len(ptr)`);
5133
+ const i = B.tmp(), lenPtr = B.tmp(), rawLen = B.tmp(), len = B.tmp();
4130
5134
  B.line(`${i} = load double, ptr ${idxSlot}`);
4131
- B.line(`${len} = call double @scr_arr_len(ptr ${arr.name})`);
5135
+ // The inline form of scr_arr_len, like the `length` intrinsic.
5136
+ B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
5137
+ this.markMemoryPointer(lenPtr, "array:header");
5138
+ B.line(`${rawLen} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`);
5139
+ B.line(`${len} = uitofp ${this.sizeType} ${rawLen} to double`);
4132
5140
  B.line(`${inBounds} = fcmp olt double ${i}, ${len}`);
4133
5141
  cur = i;
4134
5142
  }
@@ -4152,12 +5160,8 @@ export class LlEmitter {
4152
5160
  const borrowedElement = !snapshot && this.loopArrayBorrows.has(s);
4153
5161
  if (!snapshot) {
4154
5162
  const acc = elemAccess(elem);
4155
- const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
4156
5163
  const getter = borrowedElement ? "scr_arr_borrow_ref" : `scr_arr_get_${acc}`;
4157
- this.declare(`declare ${acc === "bool" ? "zeroext i1" : accTy} @${getter}(ptr, double)`);
4158
- const value = B.tmp();
4159
- B.line(`${value} = call ${accTy} @${getter}(ptr ${arr.name}, double ${cur})`);
4160
- cur = value;
5164
+ cur = emitDenseArrayGet(this, arr.name, { name: cur, type: { kind: "f64" } }, undefined, acc, elem, getter, !borrowedElement);
4161
5165
  }
4162
5166
  if (spanLength) {
4163
5167
  const length = B.tmp();
@@ -4249,7 +5253,12 @@ export class LlEmitter {
4249
5253
  // the actual ret. The parked value owns a synthetic slot-backed
4250
5254
  // scope entry during each copy so a throwing finally releases it.
4251
5255
  let v = null;
4252
- if (s.value !== null) {
5256
+ if (s.value !== null && this.currentBorrowedReturn) {
5257
+ // A borrowed-return body hands back its walk without a reference
5258
+ // (it has no try statements, so no finally can intervene).
5259
+ v = this.emitReadReceiver(s.value);
5260
+ }
5261
+ else if (s.value !== null) {
4253
5262
  v = this.emitExpr(s.value);
4254
5263
  this.moveTemp(v);
4255
5264
  }
@@ -4553,7 +5562,7 @@ export class LlEmitter {
4553
5562
  const hit = B.tmp();
4554
5563
  if (c.test.type.kind === "string") {
4555
5564
  this.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
4556
- B.line(`${hit} = call zeroext i1 @scr_str_eq(ptr ${disc.name}, ptr ${t.name})`);
5565
+ B.line(`${hit} = call zeroext i1 @sc_str_eq(ptr ${disc.name}, ptr ${t.name})`);
4557
5566
  }
4558
5567
  else if (c.test.type.kind === "bool") {
4559
5568
  B.line(`${hit} = icmp eq i1 ${disc.name}, ${t.name}`);
@@ -4624,24 +5633,62 @@ export class LlEmitter {
4624
5633
  return emitLiteralExpr(this, e);
4625
5634
  }
4626
5635
  emitOperatorExpr(e) {
4627
- if (e.kind === "seqExpr") {
4628
- // Saved operands can feed later call arguments. Transfer ownership
4629
- // to this expression's frame so they survive those reads and still
4630
- // die on the path that created them, including inside lazy branches.
4631
- this.scopes.push([]);
4632
- const result = emitBorrowedFieldSequence(this, e) ?? emitOperatorExpr(this, e);
4633
- for (const local of this.scopes.pop()) {
4634
- this.currentFrame().push({
4635
- name: local.slot,
4636
- type: local.type,
4637
- slot: true,
4638
- ...(local.boxed ? { boxed: true } : {}),
4639
- });
4640
- }
4641
- return result;
4642
- }
5636
+ if (e.kind === "seqExpr")
5637
+ return this.emitSequence(() => emitBorrowedFieldSequence(this, e) ?? emitOperatorExpr(this, e));
4643
5638
  return emitOperatorExpr(this, e);
4644
5639
  }
5640
+ /** Saved operands can feed later call arguments. Transfer ownership of a
5641
+ * sequence's locals to this expression's frame so they survive those
5642
+ * reads and still die on the path that created them, including inside
5643
+ * lazy branches. */
5644
+ emitSequence(body) {
5645
+ this.scopes.push([]);
5646
+ const result = body();
5647
+ for (const local of this.scopes.pop()) {
5648
+ this.currentFrame().push({
5649
+ name: local.slot,
5650
+ type: local.type,
5651
+ slot: true,
5652
+ ...(local.boxed ? { boxed: true } : {}),
5653
+ });
5654
+ }
5655
+ return result;
5656
+ }
5657
+ /** A statement-position expression whose value is dropped. When a
5658
+ * sequence's result only reads an unchanged local (a derived
5659
+ * constructor's `super()` evaluates to `this`), its statements still run
5660
+ * in place, but the result is neither retained nor released. */
5661
+ /** Whether a discarded value can be skipped entirely: a borrowable read
5662
+ * with no effect of its own. canBorrowCallArgument also accepts checked
5663
+ * projections (a ternary or narrow whose failing arm throws, and the
5664
+ * throwing call itself) because a borrowed use still evaluates them;
5665
+ * skipping those would drop the throw. */
5666
+ discardableRead(value) {
5667
+ if (value.kind === "libCall" || value.kind === "ternary" || value.kind === "unionNarrow")
5668
+ return false;
5669
+ if ((value.kind === "upcast" || value.kind === "downcast" || value.kind === "unionWrap") &&
5670
+ !this.discardableRead(value.value))
5671
+ return false;
5672
+ if (value.kind === "seqExpr") {
5673
+ const guarded = guardedValue(value);
5674
+ if (guarded === null || !this.discardableRead(guarded))
5675
+ return false;
5676
+ }
5677
+ return this.canBorrowCallArgument(value);
5678
+ }
5679
+ emitDiscarded(e) {
5680
+ if (e.kind === "seqExpr" &&
5681
+ isRefCounted(e.result.type) &&
5682
+ e.result.kind !== "strLit" &&
5683
+ this.discardableRead(e.result)) {
5684
+ this.emitSequence(() => {
5685
+ for (const s of e.stmts)
5686
+ this.emitStmt(s);
5687
+ });
5688
+ return;
5689
+ }
5690
+ this.emitExpr(e);
5691
+ }
4645
5692
  emitControlExpr(e) {
4646
5693
  return emitControlExpr(this, e);
4647
5694
  }
@@ -4655,8 +5702,17 @@ export class LlEmitter {
4655
5702
  * value, making the snapshot unique unless a real observable alias exists.
4656
5703
  */
4657
5704
  emitStringSelfConcatAssign(localId, left, suffix, retainForYield) {
5705
+ const moved = this.emitMovedSelfConcat(localId, left, suffix, retainForYield);
5706
+ if (moved)
5707
+ return moved;
4658
5708
  const snapshot = this.emitExpr(left);
4659
- const right = this.emitExpr(suffix);
5709
+ // A suffix of several parts, or a number, appends each operand to the
5710
+ // snapshot directly (in place once the binding lets go of it) instead
5711
+ // of first building the suffix as a separate string.
5712
+ const parts = stringParts(suffix);
5713
+ const mixed = parts.length > 1 || numberPart(parts[0]) !== null;
5714
+ const inputs = mixed ? emitConcatInputs(this, parts) : [];
5715
+ const right = mixed ? null : this.emitExpr(suffix);
4660
5716
  const b = this.binding(localId);
4661
5717
  const B = this.B;
4662
5718
  if (b.kind === "boxed") {
@@ -4669,9 +5725,15 @@ export class LlEmitter {
4669
5725
  B.line(`store ptr null, ptr ${b.slot}`);
4670
5726
  this.releaseValue(old, b.type);
4671
5727
  }
4672
- this.declare(`declare ptr @scr_str_concat(ptr, ptr)`);
4673
- const raw = B.tmp();
4674
- B.line(`${raw} = call ptr @scr_str_concat(ptr ${snapshot.name}, ptr ${right.name})`);
5728
+ let raw;
5729
+ if (right === null) {
5730
+ raw = emitMixedConcat(this, snapshot.name, inputs);
5731
+ }
5732
+ else {
5733
+ this.declare(`declare ptr @scr_str_concat(ptr, ptr)`);
5734
+ raw = B.tmp();
5735
+ B.line(`${raw} = call ptr @scr_str_concat(ptr ${snapshot.name}, ptr ${right.name})`);
5736
+ }
4675
5737
  const result = this.own({ name: raw, type: left.type });
4676
5738
  if (retainForYield) {
4677
5739
  const stored = this.retainValue(result.name, result.type);
@@ -4689,6 +5751,43 @@ export class LlEmitter {
4689
5751
  }
4690
5752
  return result;
4691
5753
  }
5754
+ /** `local = local + suffix` when the suffix cannot read or write the
5755
+ * local (an unboxed binding is only reachable through this function's own
5756
+ * expressions): reading the binding after the suffix gives the value the
5757
+ * left side had, so it moves into a consuming concat, and the result takes
5758
+ * its place. Null when the general snapshot path is needed. */
5759
+ emitMovedSelfConcat(localId, left, suffix, retainForYield) {
5760
+ const b = this.binding(localId);
5761
+ if (retainForYield ||
5762
+ b.kind !== "local" ||
5763
+ b.local === undefined ||
5764
+ b.local.boxed ||
5765
+ b.local.tdz ||
5766
+ b.type.kind !== "string" ||
5767
+ left.kind !== "varRef" ||
5768
+ left.localId !== localId ||
5769
+ mentionsLocal(suffix, localId))
5770
+ return null;
5771
+ const parts = stringParts(suffix);
5772
+ const mixed = parts.length > 1 || numberPart(parts[0]) !== null;
5773
+ const inputs = mixed ? emitConcatInputs(this, parts) : [];
5774
+ const right = mixed ? null : this.emitExpr(suffix);
5775
+ this.materializeSplitLocal(localId);
5776
+ const B = this.B;
5777
+ const head = B.tmp();
5778
+ B.line(`${head} = load ptr, ptr ${b.slot}`);
5779
+ let raw;
5780
+ if (right === null) {
5781
+ raw = emitMixedConcat(this, head, inputs, true);
5782
+ }
5783
+ else {
5784
+ this.declare(`declare ptr @scr_str_concat_move(ptr, ptr)`);
5785
+ raw = B.tmp();
5786
+ B.line(`${raw} = call ptr @scr_str_concat_move(ptr ${head}, ptr ${right.name})`);
5787
+ }
5788
+ B.line(`store ptr ${raw}, ptr ${b.slot}`);
5789
+ return { name: raw, type: left.type }; // moved into the binding
5790
+ }
4692
5791
  emitContainerExpr(e) {
4693
5792
  return emitContainerExpr(this, e);
4694
5793
  }
@@ -4702,8 +5801,152 @@ export class LlEmitter {
4702
5801
  return emitDynamicExpr(this, e);
4703
5802
  }
4704
5803
  emitIntrinsicExpr(e) {
5804
+ if (e.name === "threads.publish")
5805
+ return this.emitPublish(e);
4705
5806
  return emitIntrinsicExpr(this, e);
4706
5807
  }
5808
+ publishWalkersCache = null;
5809
+ /** Per-type publication walkers, created on the first publish. */
5810
+ get publishWalkers() {
5811
+ if (this.publishWalkersCache !== null)
5812
+ return this.publishWalkersCache;
5813
+ const byPre = [...this.classMeta.values()].sort((a, b) => a.pre - b.pre);
5814
+ this.publishWalkersCache = new PublishWalkers({
5815
+ declare: (decl) => this.declare(decl),
5816
+ cstr: (text) => this.cstr(text),
5817
+ sizeType: this.sizeType,
5818
+ tracedShapes: this.tracedShapes,
5819
+ unionsById: this.unionsById,
5820
+ recordsById: this.recordsById,
5821
+ nullableArm: (t) => this.nullableUnions.of(t)?.arm ?? null,
5822
+ classInfo: (className) => {
5823
+ const meta = this.classMetaOf(className);
5824
+ return {
5825
+ def: meta.def,
5826
+ hierarchy: meta.hierarchy,
5827
+ pre: meta.pre,
5828
+ post: meta.post,
5829
+ runtimeRooted: meta.root.def.runtime === true,
5830
+ fields: meta.def.fields.map((f) => ({
5831
+ name: f.name,
5832
+ index: classFieldIndex(meta, f.name).index,
5833
+ type: this.nullableFields.storageType(className, f.name, f.type),
5834
+ })),
5835
+ };
5836
+ },
5837
+ classesInInterval: (pre, post) => byPre.filter((m) => m.pre >= pre && m.pre <= post).map((m) => m.def.name),
5838
+ });
5839
+ return this.publishWalkersCache;
5840
+ }
5841
+ /** `publish(value)` (@scriptc/threads): makes the graph immortal and
5842
+ * immutable (scr_publish with the static type's walker) and answers the
5843
+ * same value. Throws (pending) when a dyn part is unpublishable. */
5844
+ emitPublish(e) {
5845
+ const value = this.emitExpr(e.args[0]);
5846
+ if (this.B.isTerminated())
5847
+ return value;
5848
+ const lines = this.publishWalkers.emitPublish(value.name, value.type, e.loc);
5849
+ if (lines.length > 0) {
5850
+ for (const line of lines)
5851
+ this.B.line(line.trimStart());
5852
+ // A refusal unwinds while the operand temp still owns its reference.
5853
+ this.emitPendingCheck();
5854
+ }
5855
+ // The result IS the operand: ownership moves to the result temp.
5856
+ if (!isRefCounted(value.type))
5857
+ return { name: value.name, type: e.type };
5858
+ this.moveTemp(value);
5859
+ return this.own({ name: value.name, type: e.type });
5860
+ }
5861
+ /** Branches on "`obj` is published" (rc == SIZE_MAX); leaves the builder
5862
+ * in the cold block and answers the continuation label. */
5863
+ emitPublishedTest(obj) {
5864
+ const B = this.B;
5865
+ const rc = B.tmp(), frozen = B.tmp(), expected = B.tmp();
5866
+ B.line(`${rc} = load ${this.sizeType}, ptr ${obj}`);
5867
+ B.line(`${frozen} = icmp eq ${this.sizeType} ${rc}, -1`);
5868
+ this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
5869
+ B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${frozen}, i1 false)`);
5870
+ const cold = B.newLabel("pub.frozen"), ok = B.newLabel("pub.ok");
5871
+ B.condBr(expected, cold, ok);
5872
+ B.startBlock(cold);
5873
+ return ok;
5874
+ }
5875
+ publishedNamesSym = null;
5876
+ /** The class-name table the hierarchy field guards index by preorder. */
5877
+ publishedClassNamesDefs() {
5878
+ if (this.publishedNamesSym === null)
5879
+ return [];
5880
+ const metas = [...this.classMeta.values()];
5881
+ const size = metas.reduce((max, m) => Math.max(max, m.pre + 1), 0);
5882
+ const names = new Array(size).fill("ptr null");
5883
+ for (const m of metas)
5884
+ names[m.pre] = `ptr ${this.cstr(m.def.jsName ?? m.def.name)}`;
5885
+ return [
5886
+ `${this.publishedNamesSym} = internal constant [${size} x ptr] [ ${names.join(", ")} ]`,
5887
+ ``,
5888
+ ];
5889
+ }
5890
+ /** The frozen-object guard of a class field or record field store:
5891
+ * Node's "Cannot assign to read only property" TypeError when the target
5892
+ * is published. `className` null means a record (`#<Object>`). */
5893
+ emitPublishedFieldGuard(obj, className, field) {
5894
+ const published = this.publishedTypes;
5895
+ if (published === null)
5896
+ return;
5897
+ if (className !== null && !published.classGuarded(className))
5898
+ return;
5899
+ const B = this.B;
5900
+ const ok = this.emitPublishedTest(obj);
5901
+ let owner;
5902
+ const meta = className === null ? undefined : this.classMeta.get(className);
5903
+ if (meta === undefined)
5904
+ owner = this.cstr("Object");
5905
+ else if (!meta.hierarchy)
5906
+ owner = this.cstr(meta.def.jsName ?? meta.def.name);
5907
+ else {
5908
+ // The DYNAMIC class names the object, as in Node.
5909
+ this.publishedNamesSym ??= "@sc_pub_class_names";
5910
+ const vtp = B.tmp(), vt = B.tmp(), pre = B.tmp(), slot = B.tmp(), name = B.tmp();
5911
+ B.line(`${vtp} = getelementptr inbounds ptr, ptr ${obj}, i64 1`);
5912
+ B.line(`${vt} = load ptr, ptr ${vtp}`);
5913
+ B.line(`${pre} = load ${this.sizeType}, ptr ${vt}`);
5914
+ B.line(`${slot} = getelementptr inbounds ptr, ptr ${this.publishedNamesSym}, ${this.sizeType} ${pre}`);
5915
+ B.line(`${name} = load ptr, ptr ${slot}`);
5916
+ owner = name;
5917
+ }
5918
+ this.declare(`declare void @scr_throw_published_field(ptr, ptr)`);
5919
+ B.line(`call void @scr_throw_published_field(ptr ${this.cstr(field)}, ptr ${owner})`);
5920
+ this.emitUnwind();
5921
+ B.startBlock(ok);
5922
+ }
5923
+ /** The frozen-array guard (scr_throw_published_array's ops): `index` and
5924
+ * `count` are doubles (the written index, the argument count). */
5925
+ emitPublishedArrayGuard(arr, arrType, op, index = f64Lit(0), count = f64Lit(0)) {
5926
+ if (this.publishedTypes?.containerGuarded(arrType) !== true)
5927
+ return;
5928
+ const B = this.B;
5929
+ const ok = this.emitPublishedTest(arr);
5930
+ this.declare(`declare zeroext i1 @scr_throw_published_array(i32, ptr, double, double)`);
5931
+ const threw = B.tmp();
5932
+ B.line(`${threw} = call zeroext i1 @scr_throw_published_array(i32 ${op}, ptr ${arr}, double ${index}, double ${count})`);
5933
+ const unwind = B.newLabel("pub.throw");
5934
+ B.condBr(threw, unwind, ok);
5935
+ B.startBlock(unwind);
5936
+ this.emitUnwind();
5937
+ B.startBlock(ok);
5938
+ }
5939
+ /** The published Map/Set guard: "Cannot modify a published Map/Set". */
5940
+ emitPublishedCollectionGuard(m, t) {
5941
+ if (this.publishedTypes?.containerGuarded(t) !== true)
5942
+ return;
5943
+ const B = this.B;
5944
+ const ok = this.emitPublishedTest(m);
5945
+ this.declare(`declare void @scr_throw_published_collection(i1 zeroext)`);
5946
+ B.line(`call void @scr_throw_published_collection(i1 ${t.kind === "set" ? "true" : "false"})`);
5947
+ this.emitUnwind();
5948
+ B.startBlock(ok);
5949
+ }
4707
5950
  emitSerializationExpr(e) {
4708
5951
  return emitSerializationExpr(this, e);
4709
5952
  }
@@ -4723,6 +5966,13 @@ export class LlEmitter {
4723
5966
  return this.emitExpr(e);
4724
5967
  if (e.kind === "strLit")
4725
5968
  return { name: this.internLiteral(e.value), type: e.type };
5969
+ const guarded = guardedValue(e);
5970
+ if (guarded !== null) {
5971
+ // Throwing guards run in place, exactly as the sequence emits them.
5972
+ for (const s of e.stmts)
5973
+ this.emitStmt(s);
5974
+ return this.emitReadReceiver(guarded);
5975
+ }
4726
5976
  if (e.kind === "varRef") {
4727
5977
  this.materializeSplitLocal(e.localId);
4728
5978
  const binding = this.binding(e.localId);
@@ -4744,24 +5994,58 @@ export class LlEmitter {
4744
5994
  this.B.line(`${value} = load ptr, ptr ${binding.slot}`);
4745
5995
  return { name: value, type: e.type };
4746
5996
  }
4747
- const read = matchMapRead(e, this.unionsById);
5997
+ if (e.kind === "arrayGet" && this.canBorrowReceiver(e)) {
5998
+ // The array keeps the element alive until the consumer; a missing
5999
+ // element still traps exactly like the owned read.
6000
+ const slot = this.B.slot();
6001
+ this.B.entryAllocas.push(`${slot} = alloca ptr`);
6002
+ emitBorrowedArrayRead(this, e, slot);
6003
+ const value = this.B.tmp();
6004
+ this.B.line(`${value} = load ptr, ptr ${slot}`);
6005
+ return { name: value, type: e.type };
6006
+ }
6007
+ const wrap = this.borrowableNullableWrap(e);
6008
+ if (wrap === "unit" && e.kind === "unionWrap")
6009
+ return { name: this.unitInstanceRef(e.unionId, e.tag), type: e.type };
6010
+ if (wrap === "ref" && e.kind === "unionWrap")
6011
+ return { name: this.emitReadReceiver(e.value).name, type: e.type };
6012
+ const read = matchMapRead(e, this.boxedUnionsById);
4748
6013
  if (read) {
4749
6014
  const result = emitStackMapRead(this, read);
4750
6015
  if (result.owner)
4751
6016
  this.ownSlot(result.owner.slot, result.owner.type);
4752
6017
  return result.value;
4753
6018
  }
4754
- if (canStackUnion(e, this.unionsById))
6019
+ const arrayRead = this.optionalArrayReads.get(e);
6020
+ if (arrayRead)
6021
+ return emitProjectedArrayRead(this, arrayRead);
6022
+ const narrow = this.checkedNarrows.get(e);
6023
+ const narrowed = narrow ? emitCheckedNarrowReceiver(this, narrow) : null;
6024
+ if (narrowed)
6025
+ return narrowed;
6026
+ if (canStackUnion(e, this.boxedUnionsById))
4755
6027
  return emitStackUnion(this, e).value;
4756
6028
  if (e.kind === "unionNarrow") {
4757
- const union = this.emitReadReceiver(e.value);
4758
- return { name: this.unionPeek(union.name), type: e.type };
6029
+ const union = this.emitUnionProjection(e.value);
6030
+ return { name: this.unionPeek(union.name, e.unionId), type: e.type };
6031
+ }
6032
+ if ((e.kind === "downcast" || e.kind === "upcast") && e.value.type.kind === "object") {
6033
+ // Prefix layouts: a class cast reads the operand's own pointer.
6034
+ return { name: this.emitReadReceiver(e.value).name, type: e.type };
4759
6035
  }
6036
+ // A nullable-pointer field has no box to borrow. Projections use
6037
+ // emitUnionProjection; other borrowers get an owned heap box.
6038
+ if (this.nullableFieldGet(e))
6039
+ return this.emitExpr(e);
4760
6040
  if (e.kind === "recordGet" || e.kind === "fieldGet") {
4761
6041
  const receiver = this.emitReadReceiver(e.obj);
4762
- const { ptr, type } = e.kind === "recordGet"
4763
- ? this.recordFieldPtr(receiver.name, e.shapeId, e.field)
4764
- : this.classFieldPtr(receiver.name, e.className, e.field);
6042
+ if (e.kind === "recordGet") {
6043
+ const { ptr, type } = this.recordFieldPtr(receiver.name, e.shapeId, e.field);
6044
+ return { name: this.loadRecordField(ptr, type), type: e.type };
6045
+ }
6046
+ const { ptr, type } = this.classFieldPtr(receiver.name, e.className, e.field);
6047
+ if (this.int32Slots.isField(e.className, e.field))
6048
+ return this.loadInt32Field(ptr, e.type);
4765
6049
  return { name: this.loadField(ptr, type), type: e.type };
4766
6050
  }
4767
6051
  if (e.kind === "ternary" && this.canBorrowReceiver(e.then) && this.canBorrowReceiver(e.else_)) {
@@ -4789,8 +6073,160 @@ export class LlEmitter {
4789
6073
  B.line(`${value} = load ptr, ptr ${slot}`);
4790
6074
  return { name: value, type: e.type };
4791
6075
  }
6076
+ if (e.kind === "call" && this.canBorrowReceiver(e))
6077
+ return emitBorrowedResultCall(this, e);
4792
6078
  return this.emitExpr(e);
4793
6079
  }
6080
+ /** A private stack union source with an independently owned payload. */
6081
+ canStackReceiver(e) {
6082
+ return this.isStackUnionSource(e);
6083
+ }
6084
+ /** True for union sources that emitReadReceiver can produce as a private
6085
+ * stack box with an independently owned payload (no heap union). */
6086
+ isStackUnionSource(e) {
6087
+ if (e.type.kind !== "union")
6088
+ return false;
6089
+ return (canStackUnion(e, this.boxedUnionsById) ||
6090
+ matchMapRead(e, this.boxedUnionsById) !== null ||
6091
+ this.optionalArrayReads.get(e) !== null);
6092
+ }
6093
+ /** The nullable-pointer representation behind a union-typed class field
6094
+ * read, or null for every other expression. */
6095
+ nullableFieldGet(e) {
6096
+ if (e.kind !== "fieldGet" || e.type.kind !== "union")
6097
+ return null;
6098
+ return this.nullableFields.get(e.className, e.field);
6099
+ }
6100
+ /** A union operand consumed only by a projection (tag test, payload
6101
+ * extraction, arm compare, truthiness). A nullable-pointer field read
6102
+ * becomes a private stack box whose payload is borrowed from the field:
6103
+ * the consumer must finish before any later expression can store to the
6104
+ * field, and must never retain, release, or store the box. Every other
6105
+ * operand takes the ordinary borrowed-receiver path. */
6106
+ emitUnionProjection(e) {
6107
+ const nullable = this.nullableFieldGet(e);
6108
+ if (!nullable || e.kind !== "fieldGet")
6109
+ return this.emitReadReceiver(e);
6110
+ const B = this.B;
6111
+ const receiver = this.emitReadReceiver(e.obj);
6112
+ const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field);
6113
+ const p = B.tmp();
6114
+ B.line(`${p} = load ptr, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
6115
+ return { name: this.stackNullableBox(p, nullable), type: e.type };
6116
+ }
6117
+ /** A private stack box over a nullable-pointer field read whose payload
6118
+ * is retained into the current frame: an independent owner, so the box
6119
+ * may outlive later stores to the field (call arguments, local copies).
6120
+ * The box itself must still never reach an RC entry point. */
6121
+ emitOwnedNullableStack(e) {
6122
+ const nullable = this.nullableFieldGet(e);
6123
+ if (!nullable || e.kind !== "fieldGet")
6124
+ throw new InternalCompilerError("llvm emitter bug: owned nullable stack of non-field");
6125
+ const receiver = this.emitReadReceiver(e.obj);
6126
+ const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field);
6127
+ const p = this.B.tmp();
6128
+ this.B.line(`${p} = load ptr, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
6129
+ const owner = this.own({ name: this.retainValue(p, nullable.arm), type: nullable.arm });
6130
+ return { box: this.stackNullableBox(owner.name, nullable), owner };
6131
+ }
6132
+ /** A private stack box over a borrowed nullable pointer. */
6133
+ stackNullableBox(p, nullable) {
6134
+ const B = this.B;
6135
+ const box = B.slot();
6136
+ B.entryAllocas.push(`${box} = alloca %ScrUnion`);
6137
+ const isNull = B.tmp(), tag = B.tmp(), tagPtr = B.tmp(), slot = B.tmp();
6138
+ B.line(`${isNull} = icmp eq ptr ${p}, null`);
6139
+ B.line(`${tag} = select i1 ${isNull}, i32 ${nullable.unitTag}, i32 ${nullable.refTag}`);
6140
+ B.line(`${tagPtr} = getelementptr inbounds %ScrUnion, ptr ${box}, i64 0, i32 1`);
6141
+ B.line(`store i32 ${tag}, ptr ${tagPtr}`);
6142
+ B.line(`${slot} = getelementptr inbounds %ScrUnion, ptr ${box}, i64 0, i32 5`);
6143
+ B.line(`store i64 0, ptr ${slot}`);
6144
+ B.line(`store ptr ${p}, ptr ${slot}`);
6145
+ return box;
6146
+ }
6147
+ /** The owned (+1) union value of a nullable-pointer slot: the unit arm's
6148
+ * immortal instance, or a fresh heap box around a retained instance. */
6149
+ nullableToOwnedUnion(p, nullable) {
6150
+ const B = this.B;
6151
+ const slot = B.slot();
6152
+ B.entryAllocas.push(`${slot} = alloca ptr`);
6153
+ const isNull = B.tmp();
6154
+ const unit = B.newLabel("nf.u"), ref = B.newLabel("nf.r"), join = B.newLabel("nf.j");
6155
+ B.line(`${isNull} = icmp eq ptr ${p}, null`);
6156
+ B.condBr(isNull, unit, ref);
6157
+ B.startBlock(unit);
6158
+ B.line(`store ptr ${this.unitInstanceRef(nullable.unionId, nullable.unitTag)}, ptr ${slot}`);
6159
+ B.br(join);
6160
+ B.startBlock(ref);
6161
+ const owned = this.retainValue(p, nullable.arm);
6162
+ const box = this.unionNewOwned(nullable.unionId, nullable.refTag, {
6163
+ name: owned,
6164
+ type: nullable.arm,
6165
+ });
6166
+ B.line(`store ptr ${box}, ptr ${slot}`);
6167
+ B.br(join);
6168
+ B.startBlock(join);
6169
+ const t = B.tmp();
6170
+ B.line(`${t} = load ptr, ptr ${slot}`);
6171
+ return t;
6172
+ }
6173
+ /** The owned (+1) instance pointer (NULL for the unit arm) of a union
6174
+ * box, which stays owned by its existing owner. */
6175
+ unionToNullable(u, nullable) {
6176
+ const B = this.B;
6177
+ const tag = this.unionTag(u, nullable.unionId);
6178
+ const isRef = B.tmp(), sel = B.tmp();
6179
+ B.line(`${isRef} = icmp eq i32 ${tag}, ${nullable.refTag}`);
6180
+ const payload = this.unionPeek(u, nullable.unionId);
6181
+ B.line(`${sel} = select i1 ${isRef}, ptr ${payload}, ptr null`);
6182
+ return this.retainValue(sel, nullable.arm);
6183
+ }
6184
+ /** `obj.f = value` for a nullable-pointer field: the new instance pointer
6185
+ * is produced without a union box where the value is a wrap, and as a
6186
+ * retained payload of a borrowed box otherwise. Unlink-then-release like
6187
+ * every field store. */
6188
+ emitNullableFieldStore(obj, className, field, nullable, value) {
6189
+ const B = this.B;
6190
+ let next;
6191
+ if (value.kind === "unionWrap" &&
6192
+ value.unionId === nullable.unionId &&
6193
+ (value.tag === nullable.refTag
6194
+ ? typeEquals(value.value.type, nullable.arm)
6195
+ : value.tag === nullable.unitTag)) {
6196
+ if (value.tag === nullable.refTag) {
6197
+ const inner = this.emitExpr(value.value);
6198
+ this.moveTemp(inner);
6199
+ next = inner.name;
6200
+ }
6201
+ else {
6202
+ // Unit payloads carry nothing; a void payload runs for effects.
6203
+ if (!isUnitType(value.value.type))
6204
+ this.emitExpr(value.value);
6205
+ next = "null";
6206
+ }
6207
+ }
6208
+ else if (value.kind === "varRef" && !this.canBorrowReceiver(value)) {
6209
+ next = this.unionToNullable(this.emitExpr(value).name, nullable);
6210
+ }
6211
+ else {
6212
+ next = this.unionToNullable(this.emitUnionProjection(value).name, nullable);
6213
+ }
6214
+ const { ptr } = this.classFieldPtr(obj, className, field);
6215
+ const old = B.tmp();
6216
+ B.line(`${old} = load ptr, ptr ${ptr}`);
6217
+ B.line(`store ptr ${next}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
6218
+ this.releaseValue(old, nullable.arm);
6219
+ }
6220
+ /** A union binding whose slot may hold a private stack box: stack locals
6221
+ * and projection-only parameters (callers may pass stack boxes). Such a
6222
+ * box must only be projected: never retained, released, or stored. */
6223
+ isStackUnionLocal(localId) {
6224
+ return (this.projectedParameters.has(localId) ||
6225
+ this.localStackUnions.has(localId) ||
6226
+ this.localUnionStorage.has(localId) ||
6227
+ this.localArrayReads.has(localId) ||
6228
+ this.mapReadLifetimes.locals.has(localId));
6229
+ }
4794
6230
  canBorrowReceiver(e) {
4795
6231
  switch (e.kind) {
4796
6232
  case "varRef": {
@@ -4802,20 +6238,166 @@ export class LlEmitter {
4802
6238
  isRefCounted(e.type)));
4803
6239
  }
4804
6240
  case "unionNarrow":
6241
+ // Class casts reinterpret the same pointer (prefix layouts).
6242
+ case "downcast":
6243
+ case "upcast":
4805
6244
  return this.canBorrowReceiver(e.value);
4806
6245
  case "fieldGet":
4807
6246
  case "recordGet":
4808
6247
  return this.canBorrowReceiver(e.obj);
4809
6248
  case "ternary":
4810
6249
  return this.canBorrowReceiver(e.then) && this.canBorrowReceiver(e.else_);
6250
+ case "seqExpr": {
6251
+ const guarded = guardedValue(e);
6252
+ return guarded !== null && this.canBorrowReceiver(guarded);
6253
+ }
4811
6254
  // The runtime always sets a pending exception; its unreachable
4812
6255
  // typed dummy is null and owns no receiver.
4813
6256
  case "libCall":
4814
6257
  return e.fn === "error.nodeThrow";
6258
+ case "unionWrap": {
6259
+ const wrap = this.borrowableNullableWrap(e);
6260
+ return wrap === "unit" || (wrap === "ref" && this.canBorrowReceiver(e.value));
6261
+ }
6262
+ // A borrowed-return callee preserves edges, so its result stays
6263
+ // reachable from borrowable arguments until the consumer runs.
6264
+ case "call":
6265
+ return (this.borrowedReturns.has(e.callee) &&
6266
+ e.args.every((arg) => (!isRefCounted(arg.type) || this.canBorrowReceiver(arg)) &&
6267
+ this.referenceEffects.preserves(arg)));
6268
+ // A required element read: the (borrowed) array owns the element
6269
+ // while an edge-preserving index computes.
6270
+ case "arrayGet":
6271
+ return (e.arr.type.kind === "array" &&
6272
+ isRefCounted(e.arr.type.elem) &&
6273
+ this.canBorrowReceiver(e.arr) &&
6274
+ this.referenceEffects.preserves(e.index));
6275
+ default:
6276
+ return false;
6277
+ }
6278
+ }
6279
+ /** A reference held as one plain pointer that emitReadReceiver can read
6280
+ * without owning: class instances, records, arrays, strings and
6281
+ * nullable-pointer unions. Tagged union boxes, closures and dynamic
6282
+ * values keep ordinary ownership. */
6283
+ plainReference(type) {
6284
+ switch (type.kind) {
6285
+ case "object":
6286
+ case "record":
6287
+ case "array":
6288
+ case "string":
6289
+ return true;
6290
+ case "union":
6291
+ return this.nullableUnions.has(type.unionId);
6292
+ default:
6293
+ return false;
6294
+ }
6295
+ }
6296
+ /** Functions returning a walk of their borrowed parameters return it
6297
+ * without a reference (+0) from their borrowing body; direct callers
6298
+ * either consume it as a receiver/walk or retain it at once, and the owned
6299
+ * adapter retains before releasing its parameters. Candidates preserve
6300
+ * every heap edge (so the result stays reachable from the arguments),
6301
+ * borrow every reference parameter and return plain pointers. The set
6302
+ * grows to a fixpoint because a call to a borrowed-return function with
6303
+ * walk arguments is itself a walk; walk facts are then recomputed with
6304
+ * the final set, admitting only parameters whose convention borrows. */
6305
+ analyzeBorrowedReturns() {
6306
+ const pointer = (type) => this.plainReference(type);
6307
+ const borrowedIds = (fn) => {
6308
+ const indexes = this.callLifetimes.borrowed.get(fn.name);
6309
+ return new Set(fn.params.filter((_, i) => indexes?.has(i)).map((p) => p.localId));
6310
+ };
6311
+ const hostFor = (fn) => {
6312
+ const allowed = borrowedIds(fn);
6313
+ return {
6314
+ pointerLocal: (local) => pointer(local.type),
6315
+ borrowsWithoutOwning: (e) => this.borrowsWithoutOwning(e),
6316
+ borrowedReturn: (callee) => this.borrowedReturns.has(callee),
6317
+ parameterAllowed: (id) => allowed.has(id),
6318
+ };
6319
+ };
6320
+ const preserving = [...this.fnByName.values()].filter((fn) => this.referenceEffects.functions.has(fn.name));
6321
+ const candidates = preserving.filter((fn) => {
6322
+ if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures)
6323
+ return false;
6324
+ // A borrowing vtable slot dispatches to the borrowing body (or its
6325
+ // virtual adapter) directly, and virtual callers own the result.
6326
+ if (this.implSlotBorrowed(fn.name).size > 0)
6327
+ return false;
6328
+ const indexes = this.callLifetimes.borrowed.get(fn.name);
6329
+ return (indexes !== undefined &&
6330
+ indexes.size > 0 &&
6331
+ fn.params.every((p, i) => !isRefCounted(p.type) || indexes.has(i)));
6332
+ });
6333
+ for (let changed = true; changed;) {
6334
+ changed = false;
6335
+ for (const fn of candidates) {
6336
+ if (this.borrowedReturns.has(fn.name))
6337
+ continue;
6338
+ if (!analyzeWalks(fn, hostFor(fn)).returnsWalk)
6339
+ continue;
6340
+ this.borrowedReturns.add(fn.name);
6341
+ changed = true;
6342
+ }
6343
+ }
6344
+ if (this.borrowedReturns.size === 0)
6345
+ return;
6346
+ for (const fn of preserving) {
6347
+ const walks = analyzeWalks(fn, hostFor(fn)).locals;
6348
+ if (walks.size > 0)
6349
+ this.walkBorrowsByFunction.set(fn.name, walks);
6350
+ }
6351
+ }
6352
+ /** emitReadReceiver produces this node's pointer without acquiring a
6353
+ * reference, given operands (and ternary arms) that do the same: plain
6354
+ * class/record field reads, class casts, nullable-pointer narrows and
6355
+ * wraps, and checked ternaries. Stack-boxed, map-read and boxed-field
6356
+ * sources are owned. Depends only on module facts, not the current
6357
+ * function. */
6358
+ borrowsWithoutOwning(e) {
6359
+ if (!isRefCounted(e.type) || this.isStackUnionSource(e))
6360
+ return false;
6361
+ if (matchMapRead(e, this.boxedUnionsById))
6362
+ return false;
6363
+ switch (e.kind) {
6364
+ case "fieldGet":
6365
+ return this.nullableFieldGet(e) === null;
6366
+ case "recordGet":
6367
+ return true;
6368
+ case "unionNarrow":
6369
+ return this.nullableUnions.has(e.unionId);
6370
+ case "downcast":
6371
+ case "upcast":
6372
+ return e.value.type.kind === "object";
6373
+ case "unionWrap":
6374
+ return this.borrowableNullableWrap(e) !== null;
6375
+ // The caller proves each arm separately.
6376
+ case "ternary":
6377
+ return true;
6378
+ // The caller proves the arguments are walks; their evaluation must not
6379
+ // remove an edge before the call reads them.
6380
+ case "call":
6381
+ return (this.borrowedReturns.has(e.callee) &&
6382
+ e.args.every((arg) => this.referenceEffects.preserves(arg)));
4815
6383
  default:
4816
6384
  return false;
4817
6385
  }
4818
6386
  }
6387
+ /** A wrap into a nullable-pointer union is the payload pointer itself
6388
+ * (the reference arm) or an immortal constant (a unit arm), so it can
6389
+ * borrow exactly when its payload can. Void payloads run for effects and
6390
+ * keep the ordinary path; tagged unions still construct a box. */
6391
+ borrowableNullableWrap(e) {
6392
+ if (e.kind !== "unionWrap")
6393
+ return null;
6394
+ const nullable = this.nullableUnions.get(e.unionId);
6395
+ if (!nullable)
6396
+ return null;
6397
+ if (isUnitType(e.value.type))
6398
+ return e.value.kind === "unitLit" ? "unit" : null;
6399
+ return e.tag === nullable.refTag && isRefCounted(e.value.type) ? "ref" : null;
6400
+ }
4819
6401
  materializeSplitLocal(localId) {
4820
6402
  const span = this.splitSpans.get(localId);
4821
6403
  if (span)
@@ -4902,8 +6484,8 @@ export class LlEmitter {
4902
6484
  resolveThunkFor(inner) {
4903
6485
  return resolveThunkFor(this, inner);
4904
6486
  }
4905
- tagInSet(uName, tags) {
4906
- return tagInSet(this, uName, tags);
6487
+ tagInSet(uName, unionId, tags) {
6488
+ return tagInSet(this, uName, unionId, tags);
4907
6489
  }
4908
6490
  arrPush(arr, acc, value) {
4909
6491
  return arrPush(this, arr, acc, value);
@@ -4947,8 +6529,8 @@ export class LlEmitter {
4947
6529
  emitBytesLength(elem, receiver, bytes) {
4948
6530
  return emitBytesLength(this, elem, receiver, bytes);
4949
6531
  }
4950
- emitBytesGet(elem, receiver, index, expr, inBounds = false) {
4951
- return emitBytesGet(this, elem, receiver, index, expr, inBounds);
6532
+ emitBytesGet(elem, receiver, index, expr, inBounds = false, invalidNaN = false) {
6533
+ return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN);
4952
6534
  }
4953
6535
  emitToUint32(value, expr, uint32) {
4954
6536
  return emitToUint32(this, value, expr, uint32);