@scriptc/compiler 0.2.7 → 0.2.9

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 (474) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +3 -0
  2. package/dist/backend/cycle-analysis.js +50 -17
  3. package/dist/backend/cycle-analysis.js.map +1 -1
  4. package/dist/backend/cycle-analysis.test.js +138 -14
  5. package/dist/backend/cycle-analysis.test.js.map +1 -1
  6. package/dist/backend/cycle-mutability.d.ts +31 -6
  7. package/dist/backend/cycle-mutability.js +120 -18
  8. package/dist/backend/cycle-mutability.js.map +1 -1
  9. package/dist/backend/llvm/alloc.d.ts +16 -3
  10. package/dist/backend/llvm/alloc.js +78 -23
  11. package/dist/backend/llvm/alloc.js.map +1 -1
  12. package/dist/backend/llvm/alloc.test.js +5 -5
  13. package/dist/backend/llvm/alloc.test.js.map +1 -1
  14. package/dist/backend/llvm/blocks.d.ts +9 -0
  15. package/dist/backend/llvm/blocks.js +9 -0
  16. package/dist/backend/llvm/blocks.js.map +1 -1
  17. package/dist/backend/llvm/borrowed-receivers.d.ts +7 -0
  18. package/dist/backend/llvm/borrowed-receivers.js +39 -0
  19. package/dist/backend/llvm/borrowed-receivers.js.map +1 -1
  20. package/dist/backend/llvm/byte-numbers.js +2 -3
  21. package/dist/backend/llvm/byte-numbers.js.map +1 -1
  22. package/dist/backend/llvm/call-lifetimes.d.ts +4 -1
  23. package/dist/backend/llvm/call-lifetimes.js +14 -3
  24. package/dist/backend/llvm/call-lifetimes.js.map +1 -1
  25. package/dist/backend/llvm/call-lifetimes.test.js +8 -3
  26. package/dist/backend/llvm/call-lifetimes.test.js.map +1 -1
  27. package/dist/backend/llvm/checked-narrows.d.ts +24 -0
  28. package/dist/backend/llvm/checked-narrows.js +108 -0
  29. package/dist/backend/llvm/checked-narrows.js.map +1 -0
  30. package/dist/backend/llvm/checked-narrows.test.d.ts +1 -0
  31. package/dist/backend/llvm/checked-narrows.test.js +208 -0
  32. package/dist/backend/llvm/checked-narrows.test.js.map +1 -0
  33. package/dist/backend/llvm/classes.d.ts +13 -1
  34. package/dist/backend/llvm/classes.js +32 -23
  35. package/dist/backend/llvm/classes.js.map +1 -1
  36. package/dist/backend/llvm/classes.test.js +32 -3
  37. package/dist/backend/llvm/classes.test.js.map +1 -1
  38. package/dist/backend/llvm/common.d.ts +17 -0
  39. package/dist/backend/llvm/common.js +28 -0
  40. package/dist/backend/llvm/common.js.map +1 -1
  41. package/dist/backend/llvm/debug-info.d.ts +6 -2
  42. package/dist/backend/llvm/debug-info.js +11 -1
  43. package/dist/backend/llvm/debug-info.js.map +1 -1
  44. package/dist/backend/llvm/dense-array-access.js +1 -1
  45. package/dist/backend/llvm/dense-array-access.test.js +89 -0
  46. package/dist/backend/llvm/dense-array-access.test.js.map +1 -1
  47. package/dist/backend/llvm/dyn.d.ts +8 -4
  48. package/dist/backend/llvm/dyn.js +58 -31
  49. package/dist/backend/llvm/dyn.js.map +1 -1
  50. package/dist/backend/llvm/emitter.d.ts +227 -12
  51. package/dist/backend/llvm/emitter.js +1492 -169
  52. package/dist/backend/llvm/emitter.js.map +1 -1
  53. package/dist/backend/llvm/emitter.test.js +54 -1
  54. package/dist/backend/llvm/emitter.test.js.map +1 -1
  55. package/dist/backend/llvm/expr-async.js +4 -4
  56. package/dist/backend/llvm/expr-async.js.map +1 -1
  57. package/dist/backend/llvm/expr-bytes.d.ts +1 -1
  58. package/dist/backend/llvm/expr-bytes.js +116 -32
  59. package/dist/backend/llvm/expr-bytes.js.map +1 -1
  60. package/dist/backend/llvm/expr-callbacks.js +29 -21
  61. package/dist/backend/llvm/expr-callbacks.js.map +1 -1
  62. package/dist/backend/llvm/expr-calls.d.ts +5 -0
  63. package/dist/backend/llvm/expr-calls.js +55 -10
  64. package/dist/backend/llvm/expr-calls.js.map +1 -1
  65. package/dist/backend/llvm/expr-containers.d.ts +7 -1
  66. package/dist/backend/llvm/expr-containers.js +231 -28
  67. package/dist/backend/llvm/expr-containers.js.map +1 -1
  68. package/dist/backend/llvm/expr-context.d.ts +2 -0
  69. package/dist/backend/llvm/expr-control.js +70 -14
  70. package/dist/backend/llvm/expr-control.js.map +1 -1
  71. package/dist/backend/llvm/expr-dynamic.js +75 -21
  72. package/dist/backend/llvm/expr-dynamic.js.map +1 -1
  73. package/dist/backend/llvm/expr-island.js +3 -8
  74. package/dist/backend/llvm/expr-island.js.map +1 -1
  75. package/dist/backend/llvm/expr-primitives.js +41 -12
  76. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  77. package/dist/backend/llvm/expr-primitives.test.js +50 -2
  78. package/dist/backend/llvm/expr-primitives.test.js.map +1 -1
  79. package/dist/backend/llvm/expr-records.js +28 -14
  80. package/dist/backend/llvm/expr-records.js.map +1 -1
  81. package/dist/backend/llvm/expr-stream-bridges.d.ts +3 -0
  82. package/dist/backend/llvm/expr-stream-bridges.js +114 -42
  83. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  84. package/dist/backend/llvm/expr-stream-callbacks.js +6 -3
  85. package/dist/backend/llvm/expr-stream-callbacks.js.map +1 -1
  86. package/dist/backend/llvm/layouts.d.ts +2 -1
  87. package/dist/backend/llvm/layouts.js +2 -2
  88. package/dist/backend/llvm/layouts.js.map +1 -1
  89. package/dist/backend/llvm/lib-abi.js +10 -0
  90. package/dist/backend/llvm/lib-abi.js.map +1 -1
  91. package/dist/backend/llvm/lib-filesystem.js +25 -13
  92. package/dist/backend/llvm/lib-filesystem.js.map +1 -1
  93. package/dist/backend/llvm/lib-process.js +8 -8
  94. package/dist/backend/llvm/lib-process.js.map +1 -1
  95. package/dist/backend/llvm/lib-shared.js +2 -0
  96. package/dist/backend/llvm/lib-shared.js.map +1 -1
  97. package/dist/backend/llvm/local-array-reads.d.ts +17 -6
  98. package/dist/backend/llvm/local-array-reads.js +223 -51
  99. package/dist/backend/llvm/local-array-reads.js.map +1 -1
  100. package/dist/backend/llvm/local-array-reads.test.js +7 -3
  101. package/dist/backend/llvm/local-array-reads.test.js.map +1 -1
  102. package/dist/backend/llvm/local-union-storage.js +10 -6
  103. package/dist/backend/llvm/local-union-storage.js.map +1 -1
  104. package/dist/backend/llvm/local-union-storage.test.js +3 -2
  105. package/dist/backend/llvm/local-union-storage.test.js.map +1 -1
  106. package/dist/backend/llvm/map-number-lookup.d.ts +18 -0
  107. package/dist/backend/llvm/map-number-lookup.js +62 -0
  108. package/dist/backend/llvm/map-number-lookup.js.map +1 -0
  109. package/dist/backend/llvm/map-read-lifetimes.d.ts +6 -0
  110. package/dist/backend/llvm/map-read-lifetimes.js +24 -3
  111. package/dist/backend/llvm/map-read-lifetimes.js.map +1 -1
  112. package/dist/backend/llvm/map-read-lifetimes.test.js +38 -19
  113. package/dist/backend/llvm/map-read-lifetimes.test.js.map +1 -1
  114. package/dist/backend/llvm/nullable-unions.d.ts +68 -0
  115. package/dist/backend/llvm/nullable-unions.js +132 -0
  116. package/dist/backend/llvm/nullable-unions.js.map +1 -0
  117. package/dist/backend/llvm/publish.d.ts +58 -0
  118. package/dist/backend/llvm/publish.js +286 -0
  119. package/dist/backend/llvm/publish.js.map +1 -0
  120. package/dist/backend/llvm/rebound-parameters.d.ts +12 -0
  121. package/dist/backend/llvm/rebound-parameters.js +72 -0
  122. package/dist/backend/llvm/rebound-parameters.js.map +1 -0
  123. package/dist/backend/llvm/rebound-parameters.test.d.ts +1 -0
  124. package/dist/backend/llvm/rebound-parameters.test.js +67 -0
  125. package/dist/backend/llvm/rebound-parameters.test.js.map +1 -0
  126. package/dist/backend/llvm/reference-effects.d.ts +18 -2
  127. package/dist/backend/llvm/reference-effects.js +140 -17
  128. package/dist/backend/llvm/reference-effects.js.map +1 -1
  129. package/dist/backend/llvm/reference-effects.test.js +52 -1
  130. package/dist/backend/llvm/reference-effects.test.js.map +1 -1
  131. package/dist/backend/llvm/shapes.d.ts +58 -19
  132. package/dist/backend/llvm/shapes.js +171 -62
  133. package/dist/backend/llvm/shapes.js.map +1 -1
  134. package/dist/backend/llvm/shapes.test.js +36 -11
  135. package/dist/backend/llvm/shapes.test.js.map +1 -1
  136. package/dist/backend/llvm/stack-unions.test.js +5 -2
  137. package/dist/backend/llvm/stack-unions.test.js.map +1 -1
  138. package/dist/backend/llvm/string-construction.d.ts +10 -0
  139. package/dist/backend/llvm/string-construction.js +42 -0
  140. package/dist/backend/llvm/string-construction.js.map +1 -1
  141. package/dist/backend/llvm/string-key-hash.d.ts +9 -2
  142. package/dist/backend/llvm/string-key-hash.js +20 -3
  143. package/dist/backend/llvm/string-key-hash.js.map +1 -1
  144. package/dist/backend/llvm/string-key-hash.test.js +14 -3
  145. package/dist/backend/llvm/string-key-hash.test.js.map +1 -1
  146. package/dist/backend/llvm/string-lifetimes.test.js +24 -11
  147. package/dist/backend/llvm/string-lifetimes.test.js.map +1 -1
  148. package/dist/backend/llvm/string-reads.d.ts +7 -0
  149. package/dist/backend/llvm/string-reads.js +79 -0
  150. package/dist/backend/llvm/string-reads.js.map +1 -0
  151. package/dist/backend/llvm/string-slices.js +1 -1
  152. package/dist/backend/llvm/switch-dispatch.test.js +1 -1
  153. package/dist/backend/llvm/switch-dispatch.test.js.map +1 -1
  154. package/dist/backend/llvm/test-boxed-unions.d.ts +5 -0
  155. package/dist/backend/llvm/test-boxed-unions.js +13 -0
  156. package/dist/backend/llvm/test-boxed-unions.js.map +1 -0
  157. package/dist/backend/llvm/trunc.d.ts +8 -0
  158. package/dist/backend/llvm/trunc.js +28 -0
  159. package/dist/backend/llvm/trunc.js.map +1 -0
  160. package/dist/backend/llvm/union-repr.d.ts +15 -0
  161. package/dist/backend/llvm/union-repr.js +89 -0
  162. package/dist/backend/llvm/union-repr.js.map +1 -0
  163. package/dist/backend/llvm/virtual-borrows.d.ts +31 -0
  164. package/dist/backend/llvm/virtual-borrows.js +82 -0
  165. package/dist/backend/llvm/virtual-borrows.js.map +1 -0
  166. package/dist/backend/llvm/walk-borrows.d.ts +43 -0
  167. package/dist/backend/llvm/walk-borrows.js +150 -0
  168. package/dist/backend/llvm/walk-borrows.js.map +1 -0
  169. package/dist/backend/llvm/walk-borrows.test.d.ts +1 -0
  170. package/dist/backend/llvm/walk-borrows.test.js +119 -0
  171. package/dist/backend/llvm/walk-borrows.test.js.map +1 -0
  172. package/dist/backend/llvm/walkers.d.ts +5 -1
  173. package/dist/backend/llvm/walkers.js +24 -21
  174. package/dist/backend/llvm/walkers.js.map +1 -1
  175. package/dist/backend/loop-polls.d.ts +14 -0
  176. package/dist/backend/loop-polls.js +100 -0
  177. package/dist/backend/loop-polls.js.map +1 -0
  178. package/dist/backend/loop-polls.test.d.ts +1 -0
  179. package/dist/backend/loop-polls.test.js +86 -0
  180. package/dist/backend/loop-polls.test.js.map +1 -0
  181. package/dist/backend/may-throw.d.ts +29 -1
  182. package/dist/backend/may-throw.js +365 -3
  183. package/dist/backend/may-throw.js.map +1 -1
  184. package/dist/backend/may-throw.test.js +136 -1
  185. package/dist/backend/may-throw.test.js.map +1 -1
  186. package/dist/backend/native/runtime-inputs.d.ts +1 -1
  187. package/dist/backend/native/runtime-inputs.js +1 -0
  188. package/dist/backend/native/runtime-inputs.js.map +1 -1
  189. package/dist/backend/native-codegen-core.d.ts +7 -5
  190. package/dist/backend/native-codegen-core.js +14 -9
  191. package/dist/backend/native-codegen-core.js.map +1 -1
  192. package/dist/backend/native-codegen.d.ts +2 -0
  193. package/dist/backend/native-codegen.js +21 -2
  194. package/dist/backend/native-codegen.js.map +1 -1
  195. package/dist/backend/native-codegen.test.js +41 -10
  196. package/dist/backend/native-codegen.test.js.map +1 -1
  197. package/dist/backend/native-tools.d.ts +2 -0
  198. package/dist/backend/native-tools.js +3 -0
  199. package/dist/backend/native-tools.js.map +1 -1
  200. package/dist/backend/publication.d.ts +28 -0
  201. package/dist/backend/publication.js +155 -0
  202. package/dist/backend/publication.js.map +1 -0
  203. package/dist/backend/publication.test.d.ts +1 -0
  204. package/dist/backend/publication.test.js +72 -0
  205. package/dist/backend/publication.test.js.map +1 -0
  206. package/dist/backend/runtime-abi.d.ts +2 -2
  207. package/dist/backend/runtime-abi.js +2 -2
  208. package/dist/backend/runtime-abi.js.map +1 -1
  209. package/dist/backend/runtime-pack-targets.test.js +1 -1
  210. package/dist/backend/runtime-pack-targets.test.js.map +1 -1
  211. package/dist/backend/runtime-pack.test.js +1 -1
  212. package/dist/backend/runtime-pack.test.js.map +1 -1
  213. package/dist/cli/command.js +78 -8
  214. package/dist/cli/command.js.map +1 -1
  215. package/dist/cli/output-options.d.ts +1 -0
  216. package/dist/cli/output-options.js +3 -0
  217. package/dist/cli/output-options.js.map +1 -1
  218. package/dist/cli/usage.d.ts +8 -1
  219. package/dist/cli/usage.js +14 -1
  220. package/dist/cli/usage.js.map +1 -1
  221. package/dist/compile-types.d.ts +17 -3
  222. package/dist/coverage/report.d.ts +25 -2
  223. package/dist/coverage/report.js +161 -33
  224. package/dist/coverage/report.js.map +1 -1
  225. package/dist/coverage/verdict-cache.d.ts +63 -0
  226. package/dist/coverage/verdict-cache.js +193 -0
  227. package/dist/coverage/verdict-cache.js.map +1 -0
  228. package/dist/coverage/verdict-cache.test.d.ts +1 -0
  229. package/dist/coverage/verdict-cache.test.js +52 -0
  230. package/dist/coverage/verdict-cache.test.js.map +1 -0
  231. package/dist/coverage/verdict-semantic.d.ts +2 -0
  232. package/dist/coverage/verdict-semantic.js +24 -0
  233. package/dist/coverage/verdict-semantic.js.map +1 -0
  234. package/dist/coverage/verdict-startup.d.ts +15 -0
  235. package/dist/coverage/verdict-startup.js +66 -0
  236. package/dist/coverage/verdict-startup.js.map +1 -0
  237. package/dist/diagnostics/diagnostic.d.ts +29 -1
  238. package/dist/diagnostics/diagnostic.js +74 -4
  239. package/dist/diagnostics/diagnostic.js.map +1 -1
  240. package/dist/diagnostics/envelope.d.ts +130 -0
  241. package/dist/diagnostics/envelope.js +257 -0
  242. package/dist/diagnostics/envelope.js.map +1 -0
  243. package/dist/diagnostics/envelope.test.d.ts +1 -0
  244. package/dist/diagnostics/envelope.test.js +91 -0
  245. package/dist/diagnostics/envelope.test.js.map +1 -0
  246. package/dist/diagnostics/render.d.ts +3 -0
  247. package/dist/diagnostics/render.js +13 -2
  248. package/dist/diagnostics/render.js.map +1 -1
  249. package/dist/executable/executable-cache.d.ts +2 -0
  250. package/dist/executable/executable-cache.js +3 -0
  251. package/dist/executable/executable-cache.js.map +1 -1
  252. package/dist/executable/executable-cache.test.js +1 -0
  253. package/dist/executable/executable-cache.test.js.map +1 -1
  254. package/dist/executable/prepare.d.ts +4 -0
  255. package/dist/executable/prepare.js +7 -1
  256. package/dist/executable/prepare.js.map +1 -1
  257. package/dist/frontend/analysis.js +1 -0
  258. package/dist/frontend/analysis.js.map +1 -1
  259. package/dist/frontend/lowering/array-values.d.ts +7 -0
  260. package/dist/frontend/lowering/array-values.js +157 -5
  261. package/dist/frontend/lowering/array-values.js.map +1 -1
  262. package/dist/frontend/lowering/builtins/numbers.js +29 -1
  263. package/dist/frontend/lowering/builtins/numbers.js.map +1 -1
  264. package/dist/frontend/lowering/builtins/process.d.ts +4 -0
  265. package/dist/frontend/lowering/builtins/process.js +10 -0
  266. package/dist/frontend/lowering/builtins/process.js.map +1 -1
  267. package/dist/frontend/lowering/builtins/strings.js +4 -1
  268. package/dist/frontend/lowering/builtins/strings.js.map +1 -1
  269. package/dist/frontend/lowering/builtins/workers.js +4 -2
  270. package/dist/frontend/lowering/builtins/workers.js.map +1 -1
  271. package/dist/frontend/lowering/call-arguments.js +96 -4
  272. package/dist/frontend/lowering/call-arguments.js.map +1 -1
  273. package/dist/frontend/lowering/class-assertions.d.ts +8 -0
  274. package/dist/frontend/lowering/class-assertions.js +217 -1
  275. package/dist/frontend/lowering/class-assertions.js.map +1 -1
  276. package/dist/frontend/lowering/class-dynamic-dispatch.js +8 -5
  277. package/dist/frontend/lowering/class-dynamic-dispatch.js.map +1 -1
  278. package/dist/frontend/lowering/class-method-mutations.js +88 -0
  279. package/dist/frontend/lowering/class-method-mutations.js.map +1 -1
  280. package/dist/frontend/lowering/coercions/state.d.ts +6 -0
  281. package/dist/frontend/lowering/coercions/state.js +2 -0
  282. package/dist/frontend/lowering/coercions/state.js.map +1 -1
  283. package/dist/frontend/lowering/coercions/unions.d.ts +8 -0
  284. package/dist/frontend/lowering/coercions/unions.js +67 -2
  285. package/dist/frontend/lowering/coercions/unions.js.map +1 -1
  286. package/dist/frontend/lowering/collation-repertoire.d.ts +1 -0
  287. package/dist/frontend/lowering/collation-repertoire.js +19 -0
  288. package/dist/frontend/lowering/collation-repertoire.js.map +1 -0
  289. package/dist/frontend/lowering/construction-escapes.d.ts +38 -0
  290. package/dist/frontend/lowering/construction-escapes.js +146 -0
  291. package/dist/frontend/lowering/construction-escapes.js.map +1 -0
  292. package/dist/frontend/lowering/construction-escapes.test.d.ts +1 -0
  293. package/dist/frontend/lowering/construction-escapes.test.js +150 -0
  294. package/dist/frontend/lowering/construction-escapes.test.js.map +1 -0
  295. package/dist/frontend/lowering/containers/array-callbacks.js +81 -44
  296. package/dist/frontend/lowering/containers/array-callbacks.js.map +1 -1
  297. package/dist/frontend/lowering/containers/array-construction.js +15 -2
  298. package/dist/frontend/lowering/containers/array-construction.js.map +1 -1
  299. package/dist/frontend/lowering/containers/array-methods.js +231 -12
  300. package/dist/frontend/lowering/containers/array-methods.js.map +1 -1
  301. package/dist/frontend/lowering/containers/bytes.js +15 -4
  302. package/dist/frontend/lowering/containers/bytes.js.map +1 -1
  303. package/dist/frontend/lowering/containers/callback-arguments.js +43 -1
  304. package/dist/frontend/lowering/containers/callback-arguments.js.map +1 -1
  305. package/dist/frontend/lowering/containers/string-and-regexp.js +7 -63
  306. package/dist/frontend/lowering/containers/string-and-regexp.js.map +1 -1
  307. package/dist/frontend/lowering/cycle-initialization.d.ts +6 -1
  308. package/dist/frontend/lowering/cycle-initialization.js +59 -33
  309. package/dist/frontend/lowering/cycle-initialization.js.map +1 -1
  310. package/dist/frontend/lowering/divergences.d.ts +9 -0
  311. package/dist/frontend/lowering/divergences.js +630 -0
  312. package/dist/frontend/lowering/divergences.js.map +1 -0
  313. package/dist/frontend/lowering/expressions/object-literals.js +40 -2
  314. package/dist/frontend/lowering/expressions/object-literals.js.map +1 -1
  315. package/dist/frontend/lowering/expressions/union-fields.d.ts +19 -0
  316. package/dist/frontend/lowering/expressions/union-fields.js +236 -0
  317. package/dist/frontend/lowering/expressions/union-fields.js.map +1 -0
  318. package/dist/frontend/lowering/lower-calls.d.ts +5 -0
  319. package/dist/frontend/lowering/lower-calls.js +237 -158
  320. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  321. package/dist/frontend/lowering/lower-classes.d.ts +4 -0
  322. package/dist/frontend/lowering/lower-classes.js +72 -22
  323. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  324. package/dist/frontend/lowering/lower-exprs.d.ts +19 -0
  325. package/dist/frontend/lowering/lower-exprs.js +592 -109
  326. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  327. package/dist/frontend/lowering/lower-generators.d.ts +19 -13
  328. package/dist/frontend/lowering/lower-generators.js +67 -44
  329. package/dist/frontend/lowering/lower-generators.js.map +1 -1
  330. package/dist/frontend/lowering/lower-inspect.js +3 -2
  331. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  332. package/dist/frontend/lowering/lower-island.d.ts +1 -1
  333. package/dist/frontend/lowering/lower-island.js +10 -7
  334. package/dist/frontend/lowering/lower-island.js.map +1 -1
  335. package/dist/frontend/lowering/lower-modules.js +90 -1
  336. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  337. package/dist/frontend/lowering/lower-stmts.js +47 -7
  338. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  339. package/dist/frontend/lowering/lowerer.d.ts +83 -2
  340. package/dist/frontend/lowering/lowerer.js +1158 -79
  341. package/dist/frontend/lowering/lowerer.js.map +1 -1
  342. package/dist/frontend/lowering/nan-coded-reads.d.ts +161 -0
  343. package/dist/frontend/lowering/nan-coded-reads.js +1342 -0
  344. package/dist/frontend/lowering/nan-coded-reads.js.map +1 -0
  345. package/dist/frontend/lowering/runtime-optional-bounds.d.ts +31 -0
  346. package/dist/frontend/lowering/runtime-optional-bounds.js +360 -0
  347. package/dist/frontend/lowering/runtime-optional-bounds.js.map +1 -0
  348. package/dist/frontend/lowering/runtime-optional-bounds.test.d.ts +1 -0
  349. package/dist/frontend/lowering/runtime-optional-bounds.test.js +94 -0
  350. package/dist/frontend/lowering/runtime-optional-bounds.test.js.map +1 -0
  351. package/dist/frontend/lowering/runtime-optional-elements.d.ts +117 -0
  352. package/dist/frontend/lowering/runtime-optional-elements.js +738 -0
  353. package/dist/frontend/lowering/runtime-optional-elements.js.map +1 -0
  354. package/dist/frontend/lowering/runtime-optional-guards.d.ts +36 -0
  355. package/dist/frontend/lowering/runtime-optional-guards.js +375 -0
  356. package/dist/frontend/lowering/runtime-optional-guards.js.map +1 -0
  357. package/dist/frontend/lowering/runtime-optional-guards.test.d.ts +1 -0
  358. package/dist/frontend/lowering/runtime-optional-guards.test.js +92 -0
  359. package/dist/frontend/lowering/runtime-optional-guards.test.js.map +1 -0
  360. package/dist/frontend/lowering/runtime-optional-ownership.d.ts +63 -0
  361. package/dist/frontend/lowering/runtime-optional-ownership.js +382 -0
  362. package/dist/frontend/lowering/runtime-optional-ownership.js.map +1 -0
  363. package/dist/frontend/lowering/runtime-optional-provenance.d.ts +2 -1
  364. package/dist/frontend/lowering/runtime-optional-provenance.js +2 -1
  365. package/dist/frontend/lowering/runtime-optional-provenance.js.map +1 -1
  366. package/dist/frontend/lowering/string-index-bounds.d.ts +36 -0
  367. package/dist/frontend/lowering/string-index-bounds.js +537 -0
  368. package/dist/frontend/lowering/string-index-bounds.js.map +1 -0
  369. package/dist/frontend/lowering/string-index-bounds.test.d.ts +1 -0
  370. package/dist/frontend/lowering/string-index-bounds.test.js +223 -0
  371. package/dist/frontend/lowering/string-index-bounds.test.js.map +1 -0
  372. package/dist/frontend/lowering/surfaces.js +8 -0
  373. package/dist/frontend/lowering/surfaces.js.map +1 -1
  374. package/dist/frontend/lowering/union-narrow.d.ts +2 -0
  375. package/dist/frontend/lowering/union-narrow.js +10 -6
  376. package/dist/frontend/lowering/union-narrow.js.map +1 -1
  377. package/dist/frontend/program-analysis.d.ts +10 -0
  378. package/dist/frontend/program-analysis.js +7 -0
  379. package/dist/frontend/program-analysis.js.map +1 -1
  380. package/dist/frontend/program.d.ts +25 -0
  381. package/dist/frontend/program.js +613 -17
  382. package/dist/frontend/program.js.map +1 -1
  383. package/dist/frontend/program.test.js +64 -1
  384. package/dist/frontend/program.test.js.map +1 -1
  385. package/dist/frontend/resolve.d.ts +6 -0
  386. package/dist/frontend/resolve.js +14 -0
  387. package/dist/frontend/resolve.js.map +1 -1
  388. package/dist/frontend/threads-import.d.ts +5 -0
  389. package/dist/frontend/threads-import.js +21 -0
  390. package/dist/frontend/threads-import.js.map +1 -0
  391. package/dist/frontend/type-mapper.d.ts +3 -0
  392. package/dist/frontend/type-mapper.js +72 -6
  393. package/dist/frontend/type-mapper.js.map +1 -1
  394. package/dist/frontend/worker-target.d.ts +4 -0
  395. package/dist/frontend/worker-target.js +8 -1
  396. package/dist/frontend/worker-target.js.map +1 -1
  397. package/dist/index.d.ts +7 -2
  398. package/dist/index.js +67 -17
  399. package/dist/index.js.map +1 -1
  400. package/dist/ir/build.d.ts +5 -0
  401. package/dist/ir/build.js +7 -0
  402. package/dist/ir/build.js.map +1 -1
  403. package/dist/ir/builtin-signatures.d.ts +1 -1
  404. package/dist/ir/builtin-signatures.js +5 -0
  405. package/dist/ir/builtin-signatures.js.map +1 -1
  406. package/dist/ir/constant-globals.d.ts +14 -0
  407. package/dist/ir/constant-globals.js +81 -0
  408. package/dist/ir/constant-globals.js.map +1 -0
  409. package/dist/ir/constant-globals.test.d.ts +1 -0
  410. package/dist/ir/constant-globals.test.js +97 -0
  411. package/dist/ir/constant-globals.test.js.map +1 -0
  412. package/dist/ir/final-fields.d.ts +35 -0
  413. package/dist/ir/final-fields.js +187 -0
  414. package/dist/ir/final-fields.js.map +1 -0
  415. package/dist/ir/final-fields.test.d.ts +1 -0
  416. package/dist/ir/final-fields.test.js +106 -0
  417. package/dist/ir/final-fields.test.js.map +1 -0
  418. package/dist/ir/int32-slots.d.ts +81 -0
  419. package/dist/ir/int32-slots.js +850 -0
  420. package/dist/ir/int32-slots.js.map +1 -0
  421. package/dist/ir/int32-slots.test.d.ts +1 -0
  422. package/dist/ir/int32-slots.test.js +249 -0
  423. package/dist/ir/int32-slots.test.js.map +1 -0
  424. package/dist/ir/integer-loops.d.ts +5 -0
  425. package/dist/ir/integer-loops.js +20 -4
  426. package/dist/ir/integer-loops.js.map +1 -1
  427. package/dist/ir/integer-loops.test.js +17 -0
  428. package/dist/ir/integer-loops.test.js.map +1 -1
  429. package/dist/ir/integer-ranges.d.ts +19 -3
  430. package/dist/ir/integer-ranges.js +168 -23
  431. package/dist/ir/integer-ranges.js.map +1 -1
  432. package/dist/ir/integer-ranges.test.js +74 -2
  433. package/dist/ir/integer-ranges.test.js.map +1 -1
  434. package/dist/ir/ir.d.ts +30 -7
  435. package/dist/ir/ir.js +4 -0
  436. package/dist/ir/ir.js.map +1 -1
  437. package/dist/ir/serialize.d.ts +12 -2
  438. package/dist/ir/serialize.js +190 -39
  439. package/dist/ir/serialize.js.map +1 -1
  440. package/dist/ir/serialize.test.js +65 -1
  441. package/dist/ir/serialize.test.js.map +1 -1
  442. package/dist/ir/validate.js +21 -2
  443. package/dist/ir/validate.js.map +1 -1
  444. package/dist/ir/validate.test.js +39 -0
  445. package/dist/ir/validate.test.js.map +1 -1
  446. package/dist/native/cache.d.ts +11 -1
  447. package/dist/native/cache.js +87 -2
  448. package/dist/native/cache.js.map +1 -1
  449. package/dist/native/cache.test.js +32 -1
  450. package/dist/native/cache.test.js.map +1 -1
  451. package/dist/native/cli.js +1 -1
  452. package/dist/native/cli.js.map +1 -1
  453. package/dist/native/compiler.d.ts +5 -0
  454. package/dist/native/compiler.js +55 -7
  455. package/dist/native/compiler.js.map +1 -1
  456. package/dist/native/prepare.d.ts +7 -1
  457. package/dist/native/prepare.js +65 -29
  458. package/dist/native/prepare.js.map +1 -1
  459. package/dist/native/prepare.test.js +23 -1
  460. package/dist/native/prepare.test.js.map +1 -1
  461. package/dist/native/text-file.d.ts +10 -0
  462. package/dist/native/text-file.js +41 -0
  463. package/dist/native/text-file.js.map +1 -0
  464. package/dist/startup-cache.d.ts +2 -0
  465. package/dist/startup-cache.js +2 -0
  466. package/dist/startup-cache.js.map +1 -1
  467. package/dist/text-parts.d.ts +5 -0
  468. package/dist/text-parts.js +36 -0
  469. package/dist/text-parts.js.map +1 -0
  470. package/package.json +20 -20
  471. package/surface-manifest.json +7 -1
  472. package/dist/frontend/lowering/expressions/union-field-write.d.ts +0 -8
  473. package/dist/frontend/lowering/expressions/union-field-write.js +0 -108
  474. package/dist/frontend/lowering/expressions/union-field-write.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { emitLlvmLayouts } from "./layouts.js";
2
- import { inlineAllocSupported } from "./alloc.js";
2
+ import { FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
3
3
  import { llvmBytes as llBytes } from "../literals.js";
4
4
  import { InternalCompilerError } from "../../errors.js";
5
5
  /** Lower typed IR to LLVM, using the runtime's C ABI.
@@ -19,9 +19,11 @@ import { InternalCompilerError } from "../../errors.js";
19
19
  * Dynamic values and islands share the runtime ABI; embedded npm modules use
20
20
  * compressed source tables that the runtime inflates on demand.
21
21
  */
22
+ import { createHash } from "node:crypto";
22
23
  import { deflateRawSync } from "node:zlib";
23
24
  import { endsWithJump, matchStringSelfConcat, streamTypedRefEligible, undefinedArmTag, } from "../../ir/analysis.js";
24
25
  import { emitLibraryIdentityLines } from "../library-identity-markers.js";
26
+ import { loopTestsEveryIteration } from "../loop-polls.js";
25
27
  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";
26
28
  import { moduleRuntimeFeatures, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, NPM_COMPRESS_MIN, } from "../../ir/runtime-features.js";
27
29
  import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, matchIntegerCountedForLoop, } from "../../ir/integer-loops.js";
@@ -32,8 +34,11 @@ import { emitByteWindowGuard, findInitializedByteLoopBindings, matchByteWindow,
32
34
  import { canDeferSplitPiece, emitSplitSpanNext, materializeSplitPiece, emitSplitCursor, emitSplitNext, emitSplitScratch, emitSplitSnapshot, findPrivateSplitLocals, loadSplitSnapshot, storeSplitSnapshot, stringSplit, } from "./split-loops.js";
33
35
  import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
34
36
  import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
35
- import { everyStmtList } from "../../ir/traverse.js";
36
- import { analyzeIntegerRanges } from "../../ir/integer-ranges.js";
37
+ import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
38
+ import { analyzeIntegerRanges, INT32_RANGE } from "../../ir/integer-ranges.js";
39
+ import { analyzeInt32Slots } from "../../ir/int32-slots.js";
40
+ import { analyzeFinalFields, constructorClass } from "../../ir/final-fields.js";
41
+ import { foldConstantGlobals } from "../../ir/constant-globals.js";
37
42
  import { findIntegerViews } from "./integer-views.js";
38
43
  import { findInitializerBindings, withInitializerBindings } from "../../ir/initializer-bindings.js";
39
44
  import { findConstantNumericTables } from "../../ir/constant-tables.js";
@@ -47,10 +52,13 @@ import { allocateLocalUnion, findLocalUnionStorage, storeLocalUnion, } from "./l
47
52
  import { BlockBuilder } from "./blocks.js";
48
53
  import { NullableRefFields } from "./nullable-fields.js";
49
54
  import { emitLocalArrayRead, emitProjectedArrayRead, emitBorrowedArrayRead, findLocalArrayReads, findCallArrayReads, OptionalArrayReads, } from "./local-array-reads.js";
55
+ import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js";
50
56
  import { emitDenseArrayGet, emitDenseArraySet } from "./dense-array-access.js";
51
57
  import { emitStackMapRead, findMapReadLifetimes, matchMapRead, } from "./map-read-lifetimes.js";
52
- import { emitBorrowedFieldSequence } from "./borrowed-receivers.js";
58
+ import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
53
59
  import { emitBorrowedInput } from "./borrowed-inputs.js";
60
+ import { analyzeWalks, findWalkBorrows } from "./walk-borrows.js";
61
+ import { findReboundParameters } from "./rebound-parameters.js";
54
62
  import { ReferenceEffects } from "./reference-effects.js";
55
63
  import { LlvmDebugInfo } from "./debug-info.js";
56
64
  import { StackCallbacks } from "./stack-callbacks.js";
@@ -59,18 +67,20 @@ import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callba
59
67
  import { findScalarStringSlices, emitStringSliceSnapshot, } from "./string-slices.js";
60
68
  import { LazyCaptures } from "./lazy-captures.js";
61
69
  import { StackCaptures } from "./stack-captures.js";
62
- import { ABSENT_FIELD_PAYLOAD, emitFieldAbsentTest, f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText, } from "./common.js";
70
+ import { ABSENT_FIELD_PAYLOAD, emitFieldAbsentTest, exactIndexLines, f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText, } from "./common.js";
63
71
  import { ffiExtendsNarrowIntegers } from "../targets.js";
64
72
  import { emitLiteralExpr, emitOperatorExpr, emitStringExpr, emitContainerExpr, emitRecordExpr, } from "./expr-primitives.js";
65
73
  import { emitControlExpr } from "./expr-control.js";
66
- import { emitCallExpr } from "./expr-calls.js";
74
+ import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
67
75
  import { emitDynamicExpr } from "./expr-dynamic.js";
68
76
  import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js";
77
+ import { PublishWalkers } from "./publish.js";
78
+ import { computePublishedTypes } from "../publication.js";
69
79
  import { emitJsInteropExpr, emitExpr } from "./expr-dispatch.js";
70
80
  import { emitJsMarshal, emitJsOp, emitJsExit, islandAdapter, islandTypedAdapter, } from "./expr-island.js";
71
81
  import { dynKind, raceAdapterFor, genResultThunkFor, childExitThunkFor, childExitSignalThunkFor, childDataThunkFor, execFileThunkFor, ipcMessageThunkFor, ipcSendThunkFor, emitterFixedAdapter, wrapEmitterListener, unwrapNullableClosure, closeBindThunkFor, closeOverrideWrapFor, } from "./expr-callbacks.js";
72
82
  import { streamDataAdapter, streamDoneFnFor, cryptoBytesThunkFor, fsRenameThunkFor, streamCbThunkFor, zlibBytesThunkFor, } from "./expr-stream-callbacks.js";
73
- import { emitStableReceiver, resolveThunkFor, tagInSet, arrPush, emitArrayCopyLoop, emitStrIntrinsic, emitArrIntrinsic, wrapNullable, emitMapNew, mapSet, emitMapLikeIntrinsic, emitSetNew, } from "./expr-containers.js";
83
+ import { emitStableReceiver, resolveThunkFor, tagInSet, arrPush, emitArrayCopyLoop, emitStrIntrinsic, emitArrIntrinsic, emitDiscardedSplice, wrapNullable, emitMapNew, mapSet, emitMapLikeIntrinsic, emitSetNew, } from "./expr-containers.js";
74
84
  import { emitIntegerLoopIndex, emitBytesIndex, emitBytesData, emitBytesLength, emitBytesGet, emitToUint32, emitBytesSet, emitBytesIntrinsic, } from "./expr-bytes.js";
75
85
  import { emitRegexIntrinsic, emitRecordKeyGet, keyedRecordReadInto } from "./expr-records.js";
76
86
  import { dynPromiseAdapter, streamTypedRefCommitAdapter, liveDynUnionRefAdapter, streamTypedRefBoxValue, streamTypedRefMaterializeAdapter, streamFromArrayAdapter, } from "./expr-stream-bridges.js";
@@ -81,10 +91,16 @@ import { emitNetworkHttpLibCall } from "./lib-network.js";
81
91
  import { emitAssertInspectLibCall, emitIoLibCall, emitGenericLibCall, emitLibCall, } from "./lib-dispatch.js";
82
92
  import { buildClassGraph, classMembershipIntervals, emitClassMembershipHelper, classEnvironmentIndex, classFieldIndex, classStructSym, } from "./classes.js";
83
93
  import { LlDyn } from "./dyn.js";
94
+ import { VirtualBorrows } from "./virtual-borrows.js";
84
95
  import { LlvmUnsupportedError } from "./unsupported.js";
85
96
  import { LlWalkers } from "./walkers.js";
86
- import { literalCapWord } from "./string-key-hash.js";
87
- import { arrNewCall, boxAccess, boxNewCall, boxReleaseSym, computeTraced, elemAccess, emitInlineRcHelpers, FN_ATTRS, inlineRcDecls, llFieldType, releaseSym, retainSym, traceArg, typedRefConstructor, vAdapters, } from "./shapes.js";
97
+ import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js";
98
+ import { emitNullablePresent, emitUnionPeek, emitUnionTag } from "./union-repr.js";
99
+ import { literalCapWord, literalCapWord32 } from "./string-key-hash.js";
100
+ import { NUMBER_MAP_ENTRY_DECL } from "./map-number-lookup.js";
101
+ import { STRING_READ_HELPERS } from "./string-reads.js";
102
+ import { emitConcatInputs, emitMixedConcat, numberPart, stringParts, } from "./string-construction.js";
103
+ import { arrNewCall, boxAccess, boxNewCall, boxReleaseSym, computeTraced, elemAccess, emitInlineRcHelpers, emitNullableRetainWrappers, FN_ATTRS, inlineRcDecls, llFieldType, releaseSym, retainSym, traceArg, typedRefConstructor, vAdapters, } from "./shapes.js";
88
104
  export { LlvmUnsupportedError } from "./unsupported.js";
89
105
  function ffiCallbackDummyLl(callback) {
90
106
  switch (callback.returns) {
@@ -114,13 +130,13 @@ function ffiCallbackDummyLl(callback) {
114
130
  export function emitLlvmModule(mod, options = {}) {
115
131
  // Keep source storage intact for debugger inspection in dev builds.
116
132
  return new LlEmitter(options.debugSources === undefined
117
- ? scalarizeNumericRecords(specializeNumericCalls(mod, mangleFunction))
133
+ ? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
118
134
  : mod, options).emit();
119
135
  }
120
136
  /** Keep large translation units below the host's single-string limit. */
121
137
  export function emitLlvmModuleSource(mod, options = {}) {
122
138
  const parts = new LlEmitter(options.debugSources === undefined
123
- ? scalarizeNumericRecords(specializeNumericCalls(mod, mangleFunction))
139
+ ? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
124
140
  : mod, options).emitParts();
125
141
  const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1));
126
142
  if (length <= 256 * 1024 * 1024)
@@ -130,6 +146,41 @@ export function emitLlvmModuleSource(mod, options = {}) {
130
146
  function llStrBytes(text) {
131
147
  return llBytes(Buffer.from(text, "utf8"));
132
148
  }
149
+ const NO_BORROWED = new Set();
150
+ /** Whether any node of `e` names the local (reads, writes, declarations,
151
+ * captures). */
152
+ function mentionsLocal(e, localId) {
153
+ const exprNames = (node) => {
154
+ if (node.kind === "varRef" || node.kind === "incDec" || node.kind === "assignExpr")
155
+ return node.localId === localId;
156
+ if (node.kind === "closure")
157
+ return node.captures.includes(localId);
158
+ if (node.kind === "classRef")
159
+ return node.captures?.includes(localId) === true;
160
+ return false;
161
+ };
162
+ const stmtNames = (node) => {
163
+ if (node.kind === "varDecl" ||
164
+ node.kind === "assign" ||
165
+ node.kind === "forOf" ||
166
+ node.kind === "rethrow")
167
+ return node.localId === localId;
168
+ return false;
169
+ };
170
+ let found = false;
171
+ const expr = (node) => {
172
+ if (found || exprNames(node))
173
+ return !(found = true);
174
+ return everyExprChild(node, expr, stmt);
175
+ };
176
+ const stmt = (node) => {
177
+ if (found || stmtNames(node))
178
+ return !(found = true);
179
+ return everyStmtChild(node, expr, stmt);
180
+ };
181
+ expr(e);
182
+ return found;
183
+ }
133
184
  export class LlEmitter {
134
185
  mod;
135
186
  localArrayReads = new Map();
@@ -176,19 +227,40 @@ export class LlEmitter {
176
227
  debugScope = null;
177
228
  sizeType;
178
229
  ffiExtendNarrowIntegers;
179
- cycleColorOffset;
230
+ cycleBufferedOffset;
231
+ /** Emitted ScrCycType descriptors so far (their collector statistics
232
+ * indices; see ShapeHost.nextCycKind). */
233
+ cycKinds = 0;
180
234
  wasi;
235
+ /** The target lacks a rounding instruction at its baseline CPU (x86-64
236
+ * before SSE4.1), so `llvm.trunc` would lower to a libm call: emit
237
+ * truncation inline through an integer round trip instead. */
238
+ inlineTrunc;
239
+ /** ELF worker executables give thread-locals the executable TLS models. */
240
+ executableTls;
241
+ /** Source loops enclosing the statement being emitted in the current
242
+ * function body. */
243
+ loopDepth = 0;
244
+ /** Darwin worker executables read the stack guard from a pthread
245
+ * thread-specific-data slot off the thread pointer (scr_runtime.h's
246
+ * SCR_STACK_GUARD_TSD): the architecture names the access sequence. */
247
+ stackGuardTsd;
181
248
  emitLibraryIdentity;
182
249
  runtimeAbiMarker;
250
+ detachedExit;
183
251
  /** Interned string literals: UTF-8 text → { symbol, byte length } —
184
252
  * first-use order, the runtime ABI’s determinism discipline. */
185
253
  literals = new Map();
254
+ /** Symbols of interned constant data, named by content (contentSymbol). */
255
+ contentSymbols = new Set();
186
256
  /** Interned unit-armed union instances: "unionId:tag" → symbol — one
187
257
  * immortal (rc == SIZE_MAX) static per (union, unit tag).
188
258
  * RC entry points and the collector skip immortals. */
189
259
  unitInstances = new Map();
190
260
  /** unionId → symbol of the union's immortal ABSENT field-slot instance. */
191
261
  absentInstances = new Map();
262
+ /** The nullable-union ABSENT sentinel was referenced (absentInstanceRef). */
263
+ needsNullableAbsent = false;
192
264
  immortalValues = new Set();
193
265
  /** Regex literal templates: "<flags>/<pattern>" → { symbol, interned
194
266
  * source/flags literal refs } — one immortal ScrRegex template per distinct
@@ -231,6 +303,7 @@ export class LlEmitter {
231
303
  fnDefText = new Map();
232
304
  referenceEffects;
233
305
  optionalArrayReads;
306
+ checkedNarrows;
234
307
  callArrayReads = new Map();
235
308
  mapReadLifetimes = { locals: new Map(), arguments: new Map() };
236
309
  callLifetimes;
@@ -248,6 +321,18 @@ export class LlEmitter {
248
321
  /** Parameters proven projection-only: callers may pass stack boxes. */
249
322
  projectedParameters = new Set();
250
323
  stableCallBindings = new Set();
324
+ /** Locals and rebound parameters holding borrowed pointers for their
325
+ * whole lifetime (walk-borrows.ts), per function and for the current one. */
326
+ walkBorrowsByFunction = new Map();
327
+ walkBorrows = new Set();
328
+ /** Functions whose borrowing body returns its result without a reference
329
+ * (analyzeBorrowedReturns), and whether the current body is one. */
330
+ borrowedReturns = new Set();
331
+ /** Rebound parameters that keep the borrowed convention with an owner
332
+ * slot (rebound-parameters.ts), per function, and the current owner slots. */
333
+ reboundByFunction = new Map();
334
+ reboundOwnerSlots = new Map();
335
+ currentBorrowedReturn = false;
251
336
  /** Manifest-bound native imports, used by ffiCall emission. */
252
337
  ffiByName = new Map();
253
338
  /** C-ABI callback trampolines and (for raw/no-userdata callbacks) their
@@ -266,6 +351,15 @@ export class LlEmitter {
266
351
  * checks are emitted only after calls that can actually raise. */
267
352
  mayThrow;
268
353
  indirectMayThrow;
354
+ /** Worker programs: the functions that poll for termination on entry
355
+ * (workerTerminationPolls); null outside worker programs. */
356
+ workerEntryPolls;
357
+ /** Functions that check the native stack guard on entry
358
+ * (stackCheckedFunctions); null where the runtime has no guard. */
359
+ stackChecks;
360
+ /** Static types whose values can be published (@scriptc/threads): their
361
+ * stores carry the frozen-object guard. Null without publish calls. */
362
+ publishedTypes;
269
363
  /** Method names with at least one may-throw implementation — the
270
364
  * virtualCall pending check's key. */
271
365
  mayThrowMethods = new Set();
@@ -287,6 +381,10 @@ export class LlEmitter {
287
381
  dynPromiseAdapters = new Map();
288
382
  unionsById = new Map();
289
383
  nullableFields;
384
+ /** Unions whose values are nullable arm pointers (nullable-unions.ts). */
385
+ nullableUnions;
386
+ /** The union table without nullable unions: the stack-box analyses. */
387
+ boxedUnionsById;
290
388
  recordsById = new Map();
291
389
  recordCloneShapes = new Set();
292
390
  tracedShapes;
@@ -294,6 +392,8 @@ export class LlEmitter {
294
392
  /** The class graph (buildClassGraph): preorder numbering, hierarchy
295
393
  * membership, virtual slot lists. */
296
394
  classMeta;
395
+ /** Which vtable slots borrow which parameters (virtual-borrows.ts). */
396
+ virtualBorrows;
297
397
  /** Class objects (classes as first-class values): className → the
298
398
  * interned .name literal ref — registered during body emission, the
299
399
  * statics and construct thunks assemble around the bodies. */
@@ -334,11 +434,18 @@ export class LlEmitter {
334
434
  storedSplits = new Map();
335
435
  streamingSplitsEnabled = false;
336
436
  initializerBindings;
437
+ /** Whole-program int32 storage and parameter/result facts. */
438
+ int32Slots;
337
439
  numericLocals = new Map();
338
440
  captureIds = new Set();
339
441
  /** Active induction bindings retain their width independently of size_t. */
340
442
  integerLoopBindings = new Map();
341
443
  countedLoopsEnabled = false;
444
+ /** Whole-program final reference fields (ir/final-fields.ts). */
445
+ finalFields;
446
+ /** The current function may borrow final fields of borrowable objects:
447
+ * not a constructor (where they change), not async or a generator. */
448
+ finalFieldBorrows = false;
342
449
  integerRanges = new Map();
343
450
  byteWindowsEnabled = true;
344
451
  byteWindowEntries = new Map();
@@ -380,23 +487,49 @@ export class LlEmitter {
380
487
  stmt: () => true,
381
488
  expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
382
489
  }));
490
+ this.nullableUnions = new NullableUnions(mod);
383
491
  this.debug =
384
492
  options.debugSources === undefined
385
493
  ? null
386
- : new LlvmDebugInfo(mod.sourceFile, options.debugSources, options.pointerBits, mod.unions);
494
+ : new LlvmDebugInfo(mod.sourceFile, options.debugSources, options.pointerBits, mod.unions, this.nullableUnions.armTypes());
387
495
  this.constantNumericTables = findConstantNumericTables(mod);
388
496
  this.initializerBindings = findInitializerBindings(mod);
497
+ this.int32Slots = analyzeInt32Slots(mod);
498
+ this.finalFields = analyzeFinalFields(mod);
389
499
  this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
390
500
  this.ffiExtendNarrowIntegers =
391
501
  options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
392
502
  this.wasi = options.wasi === true;
503
+ // An empty triple targets the host, as for executableTls below: the
504
+ // Node-hosted CLI passes none, the native CLI passes its toolchain's, and
505
+ // both must emit the same module for the same target.
506
+ this.inlineTrunc =
507
+ !this.wasi &&
508
+ (options.targetTriple
509
+ ? /^(x86_64|amd64)\b/i.test(options.targetTriple)
510
+ : process.arch === "x64");
511
+ this.executableTls =
512
+ mod.workers === true &&
513
+ mod.lib === undefined &&
514
+ !this.wasi &&
515
+ !/apple|darwin|windows|mingw|cygwin|win32/.test(options.targetTriple || process.platform);
516
+ const host = options.targetTriple || `${process.arch}-${process.platform}`;
517
+ this.stackGuardTsd =
518
+ mod.workers === true && mod.lib === undefined && !this.wasi && /apple|darwin/.test(host)
519
+ ? /^(arm64|aarch64)/.test(host)
520
+ ? "aarch64"
521
+ : /^(x86_64|amd64|x64)/.test(host)
522
+ ? "x86_64"
523
+ : null
524
+ : null;
393
525
  this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
394
526
  this.runtimeAbiMarker = options.runtimeAbiMarker === true;
527
+ this.detachedExit = options.detachedExit === true;
395
528
  this.rcHelpers = options.inlineRc === true ? new Set() : null;
396
- // ScrCycHdr.color stays 12 bytes behind a wasm32 object and 16 bytes
397
- // behind a 64-bit object. The wasm32 header pads before color so its
398
- // payload remains double-aligned without changing this ABI offset.
399
- this.cycleColorOffset = options.pointerBits === 32 ? 12 : 16;
529
+ // ScrCycHdr.buffered sits 6 bytes behind every object. The wasm32
530
+ // header pads before its descriptor so its payload remains
531
+ // double-aligned without changing this ABI offset.
532
+ this.cycleBufferedOffset = 6;
400
533
  this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []);
401
534
  this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
402
535
  this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []);
@@ -405,11 +538,17 @@ export class LlEmitter {
405
538
  for (const entry of mod.ffiImports ?? []) {
406
539
  this.ffiByName.set(entry.name, entry);
407
540
  }
408
- const mt = computeMayThrow(mod);
541
+ // Executables on Linux and macOS install a native stack guard per thread
542
+ // and per async fiber; WASI, Windows and library builds have none.
543
+ const triple = (options.targetTriple ?? "").toLowerCase();
544
+ const windows = triple === "" ? process.platform === "win32" : triple.includes("windows");
545
+ const mt = computeMayThrow(mod, {
546
+ stackChecks: !this.wasi && mod.lib === undefined && !windows,
547
+ });
409
548
  this.mayThrow = mt.fns;
410
- if (mod.workers)
411
- for (const fn of mod.functions)
412
- this.mayThrow.add(fn.name);
549
+ this.workerEntryPolls = mt.workerEntryPolls ?? null;
550
+ this.stackChecks = mt.stackChecks ?? null;
551
+ this.publishedTypes = computePublishedTypes(mod);
413
552
  this.indirectMayThrow = mt.indirect || mod.workers === true;
414
553
  for (const cls of mod.classes ?? []) {
415
554
  for (const m of cls.methods ?? []) {
@@ -419,10 +558,57 @@ export class LlEmitter {
419
558
  }
420
559
  for (const u of mod.unions ?? [])
421
560
  this.unionsById.set(u.id, u);
422
- this.nullableFields = new NullableRefFields(mod.classes ?? [], this.unionsById);
423
- this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById);
424
- this.referenceEffects = new ReferenceEffects(this.fnByName, (call) => this.optionalArrayReads.get(call) !== null);
425
- this.callLifetimes = analyzeCallLifetimes(this.fnByName, (className, field) => this.nullableFields.get(className, field) !== null);
561
+ this.boxedUnionsById = this.nullableUnions.boxedUnions(this.unionsById);
562
+ this.immortalValues.add("null");
563
+ this.immortalValues.add(NULLABLE_NULL);
564
+ this.nullableFields = new NullableRefFields(mod.classes ?? [], this.boxedUnionsById);
565
+ // Optional array reads also cover nullable unions: emitLocalArrayRead
566
+ // then yields the (borrowed or owned) element pointer, not a stack box.
567
+ // Arrays of such a union are read the same way.
568
+ this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById, (id) => this.nullableUnions.has(id));
569
+ this.checkedNarrows = new CheckedNarrows(this.fnByName);
570
+ this.referenceEffects = new ReferenceEffects(this.fnByName, (call) => this.optionalArrayReads.get(call) !== null, mod.classes ?? []);
571
+ const walkParameters = new Map();
572
+ if (this.debug === null) {
573
+ const host = {
574
+ pointerLocal: (local) => this.plainReference(local.type),
575
+ borrowsWithoutOwning: (e) => this.borrowsWithoutOwning(e),
576
+ };
577
+ for (const fn of this.fnByName.values()) {
578
+ if (!this.referenceEffects.functions.has(fn.name))
579
+ continue;
580
+ const walks = findWalkBorrows(fn, host);
581
+ if (walks.size === 0)
582
+ continue;
583
+ this.walkBorrowsByFunction.set(fn.name, walks);
584
+ const indexes = new Set();
585
+ fn.params.forEach((param, index) => {
586
+ if (walks.has(param.localId))
587
+ indexes.add(index);
588
+ });
589
+ if (indexes.size > 0)
590
+ walkParameters.set(fn.name, indexes);
591
+ }
592
+ }
593
+ if (this.debug === null) {
594
+ for (const fn of this.fnByName.values()) {
595
+ // Strings keep the owned parameter: their in-place append
596
+ // (`s = s + x`) needs a uniquely owned binding.
597
+ const rebound = findReboundParameters(fn, (type) => this.plainReference(type) &&
598
+ type.kind !== "string" &&
599
+ (type.kind !== "union" || this.nullableUnions.get(type.unionId)?.arm.kind !== "string"), this.walkBorrowsByFunction.get(fn.name) ?? new Set());
600
+ if (rebound.size === 0)
601
+ continue;
602
+ this.reboundByFunction.set(fn.name, rebound);
603
+ const indexes = walkParameters.get(fn.name) ?? new Set();
604
+ fn.params.forEach((param, index) => {
605
+ if (rebound.has(param.localId))
606
+ indexes.add(index);
607
+ });
608
+ walkParameters.set(fn.name, indexes);
609
+ }
610
+ }
611
+ this.callLifetimes = analyzeCallLifetimes(this.fnByName, (className, field) => this.nullableFields.get(className, field) !== null, walkParameters);
426
612
  this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes);
427
613
  this.stackCallbacks = new StackCallbacks(this.fnByName);
428
614
  this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
@@ -435,9 +621,10 @@ export class LlEmitter {
435
621
  this.tracedUnions = traced.unions;
436
622
  this.shapeHost = {
437
623
  sizeType: this.sizeType,
438
- cycleColorOffset: this.cycleColorOffset,
624
+ cycleBufferedOffset: this.cycleBufferedOffset,
439
625
  tracedShapes: this.tracedShapes,
440
626
  tracedUnions: this.tracedUnions,
627
+ nullableUnions: this.nullableUnions,
441
628
  recordsById: this.recordsById,
442
629
  recordCloneShapes: this.recordCloneShapes,
443
630
  objectAudit: options.objectAudit !== false,
@@ -445,12 +632,13 @@ export class LlEmitter {
445
632
  pointerBits: options.pointerBits,
446
633
  wasi: options.wasi,
447
634
  targetTriple: options.targetTriple,
448
- // Worker programs (SCR_WORKERS) make scr_cyc_live and
449
- // scr_weak_dispose_hook thread-local and compile the allocator out,
450
- // exactly like thread-instanced libraries.
451
- threadInstances: mod.lib?.threadInstances === true || mod.workers === true,
635
+ threadInstances: mod.lib?.threadInstances === true,
452
636
  }),
637
+ // Worker programs keep one allocator per script thread: the inline
638
+ // paths address its thread-local state.
639
+ threadLocalAlloc: mod.workers === true,
453
640
  rcHelpers: this.rcHelpers,
641
+ nextCycKind: () => FIRST_EMITTED_CYC_KIND + this.cycKinds++,
454
642
  unionsById: this.unionsById,
455
643
  declare: (decl) => this.declare(decl),
456
644
  needOom: () => this.needOom(),
@@ -458,6 +646,7 @@ export class LlEmitter {
458
646
  internLiteral: (text) => this.internLiteral(text),
459
647
  cstr: (text) => this.cstr(text),
460
648
  unitInstanceRef: (unionId, tag) => this.unitInstanceRef(unionId, tag),
649
+ fieldAbsentTestIn: (B, value, unionId) => this.fieldAbsentTestIn(B, value, unionId),
461
650
  absentInstanceRef: (unionId) => this.absentInstanceRef(unionId),
462
651
  liveDynRefAdapter: (type) => this.liveDynRefAdapter(type),
463
652
  liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type),
@@ -470,6 +659,8 @@ export class LlEmitter {
470
659
  .filter((meta) => intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post))
471
660
  .map((meta) => meta.def.name);
472
661
  },
662
+ pendingTestLines: (dest) => this.pendingTestLines(dest),
663
+ virtualEntry: (implFn) => this.virtualEntry(implFn),
473
664
  };
474
665
  this.walkers = new LlWalkers(this.shapeHost);
475
666
  this.dyn = new LlDyn(this.shapeHost);
@@ -526,6 +717,21 @@ export class LlEmitter {
526
717
  lib: rec.lib,
527
718
  });
528
719
  }
720
+ this.virtualBorrows = new VirtualBorrows(mod, this.classMeta, this.fnByName, this.callLifetimes.borrowed);
721
+ // After the vtable slots: borrowing slots exclude their implementations.
722
+ if (this.debug === null)
723
+ this.analyzeBorrowedReturns();
724
+ }
725
+ /** The borrowed parameters of a hierarchy's vtable slot (empty: owned). */
726
+ virtualSlotBorrowed(rootName, slotIndex) {
727
+ return this.virtualBorrows.slot(rootName, slotIndex);
728
+ }
729
+ /** The vtable entry for one implementation (VirtualBorrows.entry). */
730
+ virtualEntry(implFn) {
731
+ return this.virtualBorrows.entry(implFn);
732
+ }
733
+ implSlotBorrowed(implFn) {
734
+ return this.virtualBorrows.implSlotBorrowed(implFn);
529
735
  }
530
736
  abiOffset(native64, wasm32) {
531
737
  return this.sizeType === "i32" ? wasm32 : native64;
@@ -566,7 +772,6 @@ export class LlEmitter {
566
772
  const defs = [];
567
773
  if (this.ffiCallbackAdapters.size === 0)
568
774
  return { globals, defs };
569
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
570
775
  this.declare(`declare void @scr_trap(ptr)`);
571
776
  const expired = this.cstr("scriptc: native callback invoked outside its call-scoped lifetime\n");
572
777
  const released = this.cstr("scriptc: native callback invoked outside its retained lifetime\n");
@@ -684,7 +889,7 @@ export class LlEmitter {
684
889
  ? ` %cb = load ptr, ptr @${adapter.tls}`
685
890
  : adapter.global !== null
686
891
  ? ` %cb = load ptr, ptr @${adapter.global}`
687
- : ` %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:`);
892
+ : ` %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:`);
688
893
  // Validate every native pointer before allocating any copy-in value.
689
894
  // A later bad slot therefore cannot leak an earlier materialization.
690
895
  for (let i = 0; i < cb.params.length; i++) {
@@ -832,6 +1037,22 @@ export class LlEmitter {
832
1037
  return this.emitParts().join("\n");
833
1038
  }
834
1039
  emitParts() {
1040
+ const parts = this.emitModuleParts();
1041
+ if (!this.executableTls)
1042
+ return parts;
1043
+ // A worker executable's thread-locals all live in the executable's own
1044
+ // TLS block: the program's internal ones, and the runtime's external
1045
+ // ones, because the runtime is always linked statically into the
1046
+ // executable. So on ELF they can all use the local-exec model: LLVM's
1047
+ // default general-dynamic sequence (relaxed by the linker to `mov %fs:0`
1048
+ // plus an add), and even initial-exec (relaxed to `mov $offset` plus an
1049
+ // indexed access), become one %fs-relative operand LLVM can fold into
1050
+ // loads and compares. Darwin's thread-local variables have one model.
1051
+ return parts.map((part) => part
1052
+ .replace(/^(@[-$._A-Za-z0-9]+ = internal )thread_local global /gm, "$1thread_local(localexec) global ")
1053
+ .replace(/^(@[-$._A-Za-z0-9]+ = external )thread_local global /gm, "$1thread_local(localexec) global "));
1054
+ }
1055
+ emitModuleParts() {
835
1056
  // Function bodies first (the literal/unit/fn-value tables fill as they
836
1057
  // emit), then the file assembles around them — the runtime ABI’s order.
837
1058
  const fnDefs = [];
@@ -858,7 +1079,7 @@ export class LlEmitter {
858
1079
  this.declare(`declare void @scr_error_register_dyn(ptr, ptr)`);
859
1080
  errorViews.push(` call void @scr_error_register_dyn(ptr @${mangleVtInstance(meta.def.name)}, ptr @${name})`);
860
1081
  }
861
- const layouts = emitLlvmLayouts(this.shapeHost, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t), this.nullableFields);
1082
+ const layouts = emitLlvmLayouts(this.shapeHost, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t), this.nullableFields, this.int32Slots);
862
1083
  const shapes = layouts.records;
863
1084
  const classShapes = layouts.classes;
864
1085
  const classObjDefs = layouts.classObjects;
@@ -1061,12 +1282,15 @@ export class LlEmitter {
1061
1282
  const topPendingReleases = asyncEntry ? globalReleaseLines("gp") : [];
1062
1283
  const loopReportedReleases = runsLoop ? globalReleaseLines("gq") : [];
1063
1284
  // main's epilogues read the flag / the loop entry points — declared
1064
- // HERE, before the extern block flushes (a pending check usually
1065
- // declared the flag already; the Set dedupes).
1285
+ // HERE, before the extern block flushes. They run once, so they keep
1286
+ // the out-of-line scr_exc_pending call; per-call checks inline the
1287
+ // test (pendingTestLines).
1066
1288
  if (entryMayThrow || runsLoop)
1067
1289
  this.declare(`declare zeroext i1 @scr_exc_pending()`);
1068
1290
  if (runsLoop) {
1069
1291
  this.declare(`declare zeroext i1 @scr_loop_run(ptr)`);
1292
+ if (this.mod.commonJsEntry === true)
1293
+ this.declare(`declare void @scr_loop_commonjs_entry()`);
1070
1294
  this.declare(`declare zeroext i1 @scr_report_unhandled_rejections()`);
1071
1295
  this.declare(`declare void @scr_discard_unhandled_rejections()`);
1072
1296
  }
@@ -1157,6 +1381,8 @@ export class LlEmitter {
1157
1381
  const helpers = [
1158
1382
  ...this.helperDefs(),
1159
1383
  ...emitClassMembershipHelper(this.classMeta, this.sizeType),
1384
+ ...(this.publishWalkersCache?.defs() ?? []),
1385
+ ...this.publishedClassNamesDefs(),
1160
1386
  ];
1161
1387
  if (this.constantNumericTables.size > 0)
1162
1388
  this.declare(`declare double @scr_arr_get_number(ptr, double)`);
@@ -1176,16 +1402,28 @@ export class LlEmitter {
1176
1402
  `%ScrClosure = type { ${this.sizeType}, ptr, ${this.sizeType}, ptr, i32, i32, ptr }`,
1177
1403
  `%ScrFfiTable = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, i8, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`,
1178
1404
  `%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr, double, ptr, ptr }`,
1179
- // ScrArr mirrors scr_runtime.h field-for-field. Live dynamic stream
1180
- // commits swap its mutable dense, sparse, presence, and property
1181
- // storage while preserving the target object's identity.
1182
- `%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 }`,
1405
+ // ScrArr mirrors scr_runtime.h field-for-field: { rc, len, cap, elem,
1406
+ // elem_retain, elem_release, elem_trace, data, present, ext }, where
1407
+ // ext points at the out-of-line sparse, property and metadata storage
1408
+ // (NULL until first used). Live dynamic stream commits swap its dense,
1409
+ // presence and out-of-line storage while preserving the target
1410
+ // object's identity.
1411
+ `%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, ptr, ptr, ptr, ptr }`,
1183
1412
  // The ScrMap prefix { rc, key_kind, val_kind, val_retain, val_release,
1184
1413
  // val_trace, key_retain, key_release, key_trace } — the inline RC
1185
1414
  // fast paths test the two trace slots (shapes.ts).
1186
1415
  ...(this.rcHelpers === null
1187
1416
  ? []
1188
1417
  : [`%ScrMapRc = type { ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr, ptr, ptr }`]),
1418
+ // The complete ScrMap and its { key, val, hash } entry, read by the
1419
+ // inline number-key lookup (sc_map_entry_f64): entries is field 12,
1420
+ // the direct index and its length fields 18 and 19.
1421
+ ...(this.decls.has(NUMBER_MAP_ENTRY_DECL)
1422
+ ? [
1423
+ `%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} }`,
1424
+ `%ScrMapEntry = type { i64, i64, i64 }`,
1425
+ ]
1426
+ : []),
1189
1427
  // The runtime error prefix { rc, vt, name, message, code, cause } and the
1190
1428
  // class-object shape { rc, pre, post, ctor, name } — field reads on
1191
1429
  // builtin errors and classval loads GEP through these.
@@ -1231,8 +1469,12 @@ export class LlEmitter {
1231
1469
  // data (string literals, unit arms, template arrays, vtables) stays
1232
1470
  // shared.
1233
1471
  const tl = this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : "";
1234
- 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.runtimeAbiMarker && this.mod.lib === undefined
1472
+ 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
1235
1473
  ? [`declare void @${RUNTIME_ABI_MARKER}()`]
1474
+ : []),
1475
+ // Overrides the runtime's weak default (scr_lib.c).
1476
+ ...(this.detachedExit && this.mod.lib === undefined
1477
+ ? [`@scr_detached_exit = constant i8 1`]
1236
1478
  : []));
1237
1479
  for (const d of this.decls)
1238
1480
  out.push(d);
@@ -1240,17 +1482,19 @@ export class LlEmitter {
1240
1482
  const flushedDecls = new Set(this.decls);
1241
1483
  for (const table of this.constantNumericTables.values()) {
1242
1484
  const n = table.values.length;
1243
- 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:`,
1244
- // The branch must dominate fptoui: NaN/out-of-range conversion
1245
- // would produce poison. Fractional indices also need a fallback.
1246
- ` %index = fptoui double %i to ${this.sizeType}`, ` %roundtrip = uitofp ${this.sizeType} %index to double`, ` %integer = fcmp oeq double %roundtrip, %i`, ` br i1 %integer, label %read, label %fallback`, `read:`, ` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, ${this.sizeType} 0, ${this.sizeType} %index`, ` %value = load double, ptr %slot`, ` ret double %value`, `fallback:`, ` %generic = call double @scr_arr_get_number(ptr %a, double %i)`, ` ret double %generic`, `}`, ``);
1485
+ let tableTmp = 0;
1486
+ const tableIndex = exactIndexLines("%i", String(n), "i64", () => `%ix${tableTmp++}`);
1487
+ 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`, ...tableIndex.lines.map((line) => ` ${line}`), ` %safe = and i1 %initialized, ${tableIndex.ok}`, ` br i1 %safe, label %read, label %fallback`, `read:`, ` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, i64 0, i64 ${tableIndex.wide}`, ` %value = load double, ptr %slot`, ` ret double %value`, `fallback:`, ` %generic = call double @scr_arr_get_number(ptr %a, double %i)`, ` ret double %generic`, `}`, ``);
1247
1488
  }
1248
1489
  for (const [text, lit] of this.literals) {
1249
1490
  // Immortal interned ScrStr: { rc = SIZE_MAX, len, cap = len, bytes\0 } —
1250
1491
  // the runtime ABI’s static table, retain/release skip rc == SIZE_MAX.
1251
1492
  // 64-bit (little-endian) targets split the capacity word: the high
1252
1493
  // half carries the precomputed Map key hash (SCR_STR_HASH_CACHE).
1253
- const capWord = this.sizeType === "i64" ? literalCapWord(Buffer.from(text, "utf8")) : String(lit.len);
1494
+ // Bit 31 marks all-ASCII literals (SCR_STR_ASCII_BIT) on every target.
1495
+ const capWord = this.sizeType === "i64"
1496
+ ? literalCapWord(Buffer.from(text, "utf8"))
1497
+ : literalCapWord32(Buffer.from(text, "utf8"));
1254
1498
  out.push(`@${lit.sym} = internal global { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, [${lit.len + 1} x i8] } ` +
1255
1499
  `{ ${this.sizeType} -1, ${this.sizeType} ${lit.len}, ${this.sizeType} ${capWord}, [${lit.len + 1} x i8] c"${llStrBytes(text)}" }`);
1256
1500
  }
@@ -1276,6 +1520,20 @@ export class LlEmitter {
1276
1520
  }
1277
1521
  if (this.absentInstances.size > 0)
1278
1522
  out.push(``);
1523
+ if (this.nullableUnions.usesNullSentinel) {
1524
+ // `null` in every `C | null | undefined` union (NULL is undefined):
1525
+ // one module-scope immortal object like the ABSENT sentinel.
1526
+ const S = this.sizeType;
1527
+ out.push(`${NULLABLE_NULL} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable null arm`, ``);
1528
+ }
1529
+ if (this.needsNullableAbsent) {
1530
+ // The ABSENT field-slot state of every nullable-pointer union: one
1531
+ // module-scope immortal object (rc == SIZE_MAX), distinct from NULL
1532
+ // (undefined) and from every instance. RC entry points and the
1533
+ // collector skip it like any immortal; field reads compare its address.
1534
+ const S = this.sizeType;
1535
+ out.push(`${NULLABLE_ABSENT} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable absent field`, ``);
1536
+ }
1279
1537
  for (const [key, re] of this.regexInstances) {
1280
1538
  // One immortal ScrRegex per (pattern, flags) literal, pointing at
1281
1539
  // the interned source/flags strings. The bc and native-matcher slots
@@ -1293,7 +1551,7 @@ export class LlEmitter {
1293
1551
  // strings — a no-op.
1294
1552
  const n = inst.slots.length;
1295
1553
  const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`;
1296
- out.push(`@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`, `@${inst.sym}_present = internal constant [${n} x i8] ${present}`, `@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, ptr null, ptr null, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null, ${this.sizeType} 0, ${this.sizeType} 0, ptr null, ${this.sizeType} 0, ${this.sizeType} 0, ptr null }`);
1554
+ out.push(`@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`, `@${inst.sym}_present = internal constant [${n} x i8] ${present}`, `@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, ptr null, ptr null, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`);
1297
1555
  }
1298
1556
  if (this.templateStringsInstances.size > 0)
1299
1557
  out.push(``);
@@ -1442,7 +1700,10 @@ export class LlEmitter {
1442
1700
  out.push(this.debug.render());
1443
1701
  return out;
1444
1702
  }
1445
- 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()`, ...stamps,
1703
+ 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()`,
1704
+ // Each context (the main thread, every worker) installs its stack
1705
+ // guard before running compiled code that checks it.
1706
+ ...(this.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []), ...stamps,
1446
1707
  // Event-surface programs (signal/exit listeners) fill the loop's
1447
1708
  // nullable event hooks before %main — scr_events.c links only when
1448
1709
  // this line is emitted (native/executable.ts gates on the same predicate).
@@ -1487,6 +1748,7 @@ export class LlEmitter {
1487
1748
  // both exit 1, like Node — the C main's loop block exactly.
1488
1749
  ...(runsLoop
1489
1750
  ? [
1751
+ ...(this.mod.commonJsEntry === true ? [` call void @scr_loop_commonjs_entry()`] : []),
1490
1752
  ` %loop_rejection = call zeroext i1 @scr_loop_run(ptr ${asyncEntry ? "%top" : "null"})`,
1491
1753
  ` %lexc = call zeroext i1 @scr_exc_pending()`,
1492
1754
  ` br i1 %lexc, label %luncaught, label %lok`,
@@ -1802,10 +2064,25 @@ export class LlEmitter {
1802
2064
  msgHelper("sc_bad_key", "sc_bad_key_msg", "scriptc: TypeError: record has no key (typed slot — no undefined is representable)\n");
1803
2065
  }
1804
2066
  if (this.needsRetainBox) {
1805
- // scr_box_retain is a static inline (increment-unless-immortal, then
1806
- // mark the cycle header live — every box is collector-headered);
2067
+ // scr_box_retain is a static inline (increment-unless-immortal);
1807
2068
  // emitted once with internal linkage, like the record retains.
1808
- 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`, `}`, ``);
2069
+ 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`, `}`, ``);
2070
+ }
2071
+ // Number-key Map/Set lookups probe the runtime's direct index inline:
2072
+ // while ndense is nonzero every live key is an integer below it, so an
2073
+ // in-range integral key reads its slot (entry index + 1, 0 = absent) and
2074
+ // any other key is absent. Maps without the index (ndense 0) and every
2075
+ // non-integral probe outside the table take the runtime lookup, which
2076
+ // keeps the complete SameValueZero semantics.
2077
+ if (this.decls.has(NUMBER_MAP_ENTRY_DECL)) {
2078
+ const sz = this.sizeType;
2079
+ let mapTmp = 0;
2080
+ const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`);
2081
+ 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`,
2082
+ // A non-integral key inside the table is as absent as one outside it.
2083
+ ...mapIndex.lines.map((line) => ` ${line}`), ` br i1 ${mapIndex.ok}, label %slot, label %outside`, `slot:`, sz === "i64"
2084
+ ? ` %i = add i64 ${mapIndex.wide}, 0`
2085
+ : ` %i = trunc i64 ${mapIndex.wide} to ${sz}`, ` %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`, `}`, ``);
1809
2086
  }
1810
2087
  // String comparisons enter through small internal wrappers that LLVM
1811
2088
  // inlines at each site: interned literals and shared values compare by
@@ -1814,6 +2091,15 @@ export class LlEmitter {
1814
2091
  if (this.decls.has(`declare zeroext i1 @scr_str_eq(ptr, ptr)`)) {
1815
2092
  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`, `}`, ``);
1816
2093
  }
2094
+ // String length and charCodeAt read proven-ASCII strings inline: bit 31
2095
+ // of the capacity word (SCR_STR_ASCII_BIT, the low half's sign bit on
2096
+ // little-endian targets) makes UTF-16 units bytes. Unproven strings and
2097
+ // anything but an in-range index take the runtime's UTF-16 mapping.
2098
+ for (const helper of STRING_READ_HELPERS) {
2099
+ if (!this.decls.has(helper.decl))
2100
+ continue;
2101
+ defs.push(...helper.define(this.sizeType, FN_ATTRS), ``);
2102
+ }
1817
2103
  // Ordering also decides inline when the first bytes differ: byte order
1818
2104
  // is the plain comparison's order, and the UTF-16 order whenever both
1819
2105
  // bytes are ASCII. Callers only test the sign of the result.
@@ -1861,7 +2147,18 @@ export class LlEmitter {
1861
2147
  * here too. */
1862
2148
  inlineRcTail(flushedDecls) {
1863
2149
  const late = inlineRcDecls(this.shapeHost).filter((d) => !flushedDecls.has(d));
1864
- return [...late, ...emitInlineRcHelpers(this.shapeHost)];
2150
+ // NULL-skipping retain wrappers of nullable unions; a runtime inner
2151
+ // retain requested after the extern block flushed is declared here.
2152
+ for (const inner of this.nullableUnions.retainWrappers.values()) {
2153
+ const decl = `declare ptr ${inner}(ptr)`;
2154
+ if (inner.startsWith("@scr_") && !flushedDecls.has(decl) && !late.includes(decl))
2155
+ late.push(decl);
2156
+ }
2157
+ return [
2158
+ ...late,
2159
+ ...emitNullableRetainWrappers(this.shapeHost),
2160
+ ...emitInlineRcHelpers(this.shapeHost),
2161
+ ];
1865
2162
  }
1866
2163
  /** Env-signature wrappers + interned immortal closures for declared
1867
2164
  * functions used as values (the runtime ABI’s sc_w_/sc_fc_ pair): every
@@ -1892,7 +2189,6 @@ export class LlEmitter {
1892
2189
  const sizeOf = `ptrtoint (ptr getelementptr (%${pack}, ptr null, i32 1) to ${this.sizeType})`;
1893
2190
  this.declare(`declare void @free(ptr)`);
1894
2191
  this.declare(`declare ptr @malloc(${this.sizeType})`);
1895
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
1896
2192
  const tr = [
1897
2193
  `define internal void @${mangleTrampoline(fn.name)}(ptr %self, ptr %ap) ${FN_ATTRS} {`,
1898
2194
  `entry:`,
@@ -2048,7 +2344,6 @@ export class LlEmitter {
2048
2344
  this.declare(`declare void @scr_async_inline_enter(ptr)`);
2049
2345
  this.declare(`declare ptr @scr_async_inline_leave(ptr)`);
2050
2346
  this.declare(`declare void @scr_promise_reject_pending(ptr)`);
2051
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
2052
2347
  if (lifted) {
2053
2348
  this.declare(`declare ptr @scr_closure_retain_v(ptr)`);
2054
2349
  this.declare(`declare void @scr_closure_release(ptr)`);
@@ -2064,7 +2359,7 @@ export class LlEmitter {
2064
2359
  ` call void @scr_async_inline_enter(ptr %frame)`,
2065
2360
  retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
2066
2361
  ...(lifted ? [` call void @scr_closure_release(ptr %a0)`] : []),
2067
- ` %pend = call zeroext i1 @scr_exc_pending()`,
2362
+ ...this.pendingTestLines("%pend"),
2068
2363
  ` %p = call ptr @scr_async_inline_leave(ptr %frame)`,
2069
2364
  ` br i1 %pend, label %thrown, label %clean`,
2070
2365
  `clean:`,
@@ -2123,7 +2418,7 @@ export class LlEmitter {
2123
2418
  if (fn.captures !== undefined) {
2124
2419
  this.declare(`declare void @scr_closure_release(ptr)`);
2125
2420
  }
2126
- 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)`);
2421
+ tr.push(...this.pendingTestLines("%pend"), ` br i1 %pend, label %thrown, label %clean`, `clean:`, ` %pr = call ptr @scr_fiber_promise(ptr %self)`);
2127
2422
  for (const line of this.asyncFulfillLines(ret, "%pr"))
2128
2423
  tr.push(line);
2129
2424
  tr.push(` ret void`, `thrown:`);
@@ -2232,7 +2527,7 @@ export class LlEmitter {
2232
2527
  tr.push(` ret void`, `}`, ``);
2233
2528
  }
2234
2529
  else {
2235
- 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:`);
2530
+ tr.push(` %g = call ptr @scr_gen_of_fiber(ptr %self)`, ...this.pendingTestLines("%pend"), ` br i1 %pend, label %thrown, label %clean`, `clean:`);
2236
2531
  switch (ret.kind) {
2237
2532
  case "void":
2238
2533
  tr.push(` br label %done ; void body: the done value is undefined (NONE)`);
@@ -2322,10 +2617,30 @@ export class LlEmitter {
2322
2617
  return out;
2323
2618
  }
2324
2619
  // ── plumbing (frame and scope ownership) ──────
2620
+ /** A symbol derived from the constant's text rather than its first-use
2621
+ * index, so an edit that interns one more string leaves every other
2622
+ * constant's symbol, and the code referencing it, unchanged. This lets
2623
+ * native builds reuse the compiled partitions an edit does not touch. A
2624
+ * collision retries with a counter, still in first-use order. */
2625
+ contentSymbol(prefix, text) {
2626
+ for (let attempt = 0;; attempt++) {
2627
+ const digest = createHash("sha256")
2628
+ .update(attempt === 0 ? text : `${attempt}\0${text}`)
2629
+ .digest("hex");
2630
+ const symbol = `${prefix}${parseInt(digest.slice(0, 13), 16)}`;
2631
+ if (!this.contentSymbols.has(symbol)) {
2632
+ this.contentSymbols.add(symbol);
2633
+ return symbol;
2634
+ }
2635
+ }
2636
+ }
2325
2637
  internLiteral(text) {
2326
2638
  let lit = this.literals.get(text);
2327
2639
  if (!lit) {
2328
- lit = { sym: `sc_lit_${this.literals.size}`, len: Buffer.byteLength(text, "utf8") };
2640
+ lit = {
2641
+ sym: this.contentSymbol("sc_lit_", text),
2642
+ len: Buffer.byteLength(text, "utf8"),
2643
+ };
2329
2644
  this.literals.set(text, lit);
2330
2645
  }
2331
2646
  const name = `@${lit.sym}`;
@@ -2337,7 +2652,7 @@ export class LlEmitter {
2337
2652
  cstr(text) {
2338
2653
  let c = this.cstrs.get(text);
2339
2654
  if (!c) {
2340
- c = { sym: `sc_cs_${this.cstrs.size}`, len: Buffer.byteLength(text, "utf8") };
2655
+ c = { sym: this.contentSymbol("sc_cs_", text), len: Buffer.byteLength(text, "utf8") };
2341
2656
  this.cstrs.set(text, c);
2342
2657
  }
2343
2658
  return `@${c.sym}`;
@@ -2354,6 +2669,12 @@ export class LlEmitter {
2354
2669
  if (!arm || !isUnitType(arm)) {
2355
2670
  throw new InternalCompilerError(`llvm emitter bug: unit instance for non-unit arm ${tag} of ${unionId}`);
2356
2671
  }
2672
+ const nullable = this.nullableUnions.get(unionId);
2673
+ if (nullable) {
2674
+ // NULL is the unit arm of a nullable union (`undefined` when it has
2675
+ // both); the `null` sentinel is immortal. Neither owns anything.
2676
+ return tag === nullable.nullTag ? NULLABLE_NULL : "null";
2677
+ }
2357
2678
  const key = `${unionId}:${tag}`;
2358
2679
  let sym = this.unitInstances.get(key);
2359
2680
  if (!sym) {
@@ -2370,6 +2691,11 @@ export class LlEmitter {
2370
2691
  if (undefinedArmTag({ kind: "union", unionId }, this.unionsById) < 0) {
2371
2692
  throw new InternalCompilerError(`llvm emitter bug: absent state of ${unionId} without undefined`);
2372
2693
  }
2694
+ if (this.nullableUnions.has(unionId)) {
2695
+ this.needsNullableAbsent = true;
2696
+ this.immortalValues.add(NULLABLE_ABSENT);
2697
+ return NULLABLE_ABSENT;
2698
+ }
2373
2699
  let sym = this.absentInstances.get(unionId);
2374
2700
  if (!sym) {
2375
2701
  sym = `sc_absent_${this.absentInstances.size}`;
@@ -2382,7 +2708,18 @@ export class LlEmitter {
2382
2708
  /** i1: a union value loaded from a field slot is the ABSENT state — the
2383
2709
  * undefined arm's tag with the absent payload marker. */
2384
2710
  fieldAbsentTest(value, unionId) {
2385
- return emitFieldAbsentTest(this.B, value, undefinedArmTag({ kind: "union", unionId }, this.unionsById));
2711
+ return this.fieldAbsentTestIn(this.B, value, unionId);
2712
+ }
2713
+ /** fieldAbsentTest in another block builder (helper generators). A
2714
+ * nullable union's ABSENT state is the module's immortal sentinel. */
2715
+ fieldAbsentTestIn(B, value, unionId) {
2716
+ if (this.nullableUnions.has(unionId)) {
2717
+ this.needsNullableAbsent = true;
2718
+ const t = B.tmp();
2719
+ B.line(`${t} = icmp eq ptr ${value}, ${NULLABLE_ABSENT}`);
2720
+ return t;
2721
+ }
2722
+ return emitFieldAbsentTest(B, value, undefinedArmTag({ kind: "union", unionId }, this.unionsById));
2386
2723
  }
2387
2724
  /** Loads a record field slot as a VALUE: an undefined-armed union slot
2388
2725
  * holding the ABSENT state surfaces as the ordinary undefined arm, so
@@ -2410,9 +2747,20 @@ export class LlEmitter {
2410
2747
  throw new InternalCompilerError("llvm emitter bug: no active statement frame");
2411
2748
  return frame;
2412
2749
  }
2750
+ /** Whether a value is immortal: an interned constant, or a temp marked by
2751
+ * markImmortal. */
2752
+ isImmortal(name) {
2753
+ return this.immortalValues.has(name) || this.B.immortalTemps.has(name);
2754
+ }
2755
+ /** Record that a temp holds an immortal value (a choice between interned
2756
+ * literals): nothing owns it, and retains, releases and moves skip it. */
2757
+ markImmortal(v) {
2758
+ this.B.immortalTemps.add(v.name);
2759
+ return v;
2760
+ }
2413
2761
  /** Registers an owned refcounted value on the current statement frame. */
2414
2762
  own(v) {
2415
- if (isRefCounted(v.type) && !this.immortalValues.has(v.name))
2763
+ if (isRefCounted(v.type) && !this.isImmortal(v.name))
2416
2764
  this.currentFrame().push(v);
2417
2765
  return v;
2418
2766
  }
@@ -2424,7 +2772,7 @@ export class LlEmitter {
2424
2772
  }
2425
2773
  /** Strike a refcounted temp from its frame: ownership is being moved. */
2426
2774
  moveTemp(v) {
2427
- if (!isRefCounted(v.type) || this.immortalValues.has(v.name))
2775
+ if (!isRefCounted(v.type) || this.isImmortal(v.name))
2428
2776
  return;
2429
2777
  for (let i = this.frames.length - 1; i >= 0; i--) {
2430
2778
  const idx = this.frames[i].findIndex((e) => e.name === v.name);
@@ -2438,7 +2786,7 @@ export class LlEmitter {
2438
2786
  /** The retained (+1) read of a refcounted value — type-directed through
2439
2787
  * the `_v` table (immortals skip, exactly the C retain calls). */
2440
2788
  retainValue(name, type) {
2441
- if (this.immortalValues.has(name))
2789
+ if (this.isImmortal(name))
2442
2790
  return name;
2443
2791
  const t = this.B.tmp();
2444
2792
  this.B.line(`${t} = call ptr ${retainSym(this.shapeHost, type)}(ptr ${name})`);
@@ -2447,7 +2795,7 @@ export class LlEmitter {
2447
2795
  /** The release call for one owned refcounted value — type-directed like
2448
2796
  * releaseCallC (all runtime releases are NULL-tolerant). */
2449
2797
  releaseValue(name, type) {
2450
- if (this.immortalValues.has(name))
2798
+ if (this.isImmortal(name))
2451
2799
  return;
2452
2800
  this.B.line(`call void ${releaseSym(this.shapeHost, type)}(ptr ${name})`);
2453
2801
  }
@@ -2472,7 +2820,21 @@ export class LlEmitter {
2472
2820
  for (const e of scope) {
2473
2821
  const t = this.B.tmp();
2474
2822
  this.B.line(`${t} = load ptr, ptr ${e.slot}`);
2475
- if (e.boxed) {
2823
+ if (e.boxed && e.lazy) {
2824
+ // Unreachable code after a terminator emits nothing; a new block
2825
+ // here would revive it.
2826
+ if (this.B.isTerminated())
2827
+ continue;
2828
+ const created = this.B.tmp();
2829
+ this.B.line(`${created} = icmp ne ptr ${t}, null`);
2830
+ const release = this.B.newLabel("lazy.release"), join = this.B.newLabel("lazy.release.j");
2831
+ this.B.condBr(created, release, join);
2832
+ this.B.startBlock(release);
2833
+ this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`);
2834
+ this.B.br(join);
2835
+ this.B.startBlock(join);
2836
+ }
2837
+ else if (e.boxed) {
2476
2838
  this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`);
2477
2839
  }
2478
2840
  else {
@@ -2523,16 +2885,34 @@ export class LlEmitter {
2523
2885
  * function — and branch to the handler / return a dummy value (never
2524
2886
  * read: callers of a may-throw function test the pending flag before
2525
2887
  * using the result). Callers own the surrounding pending branch; a
2526
- * `throw` unwinds unconditionally. */
2527
- emitUnwind() {
2888
+ * `throw` unwinds unconditionally. `passThrough` names a scalar call
2889
+ * result of the function's own return type: with nothing to release, the
2890
+ * unwind returns it as the dummy, so a call in tail position keeps a single
2891
+ * return and stays eligible for tail-call and recursion elimination. */
2892
+ emitUnwind(passThrough) {
2528
2893
  if (this.mod.workers && this.tryStack.length > 0) {
2529
- this.declare(`declare zeroext i1 @scr_context_stopping()`);
2530
- const stopped = this.B.tmp();
2531
- this.B.line(`${stopped} = call zeroext i1 @scr_context_stopping()`);
2532
2894
  const stop = this.B.newLabel("context.stop");
2533
2895
  const ordinary = this.B.newLabel("context.throw");
2534
- this.B.condBr(stopped, stop, ordinary);
2535
- this.B.startBlock(stop);
2896
+ if (this.executableTls) {
2897
+ // A set stop bit unwinds while the active cell may still be clear:
2898
+ // install the termination sentinel before skipping the handlers.
2899
+ const word = this.loadExcFlag();
2900
+ const bit = this.B.tmp();
2901
+ const stopped = this.B.tmp();
2902
+ this.B.line(`${bit} = and i32 ${word}, 2`);
2903
+ this.B.line(`${stopped} = icmp ne i32 ${bit}, 0`);
2904
+ this.B.condBr(stopped, stop, ordinary);
2905
+ this.B.startBlock(stop);
2906
+ this.declare(`declare zeroext i1 @scr_context_checkpoint()`);
2907
+ this.B.line(`call zeroext i1 @scr_context_checkpoint()`);
2908
+ }
2909
+ else {
2910
+ this.declare(`declare zeroext i1 @scr_context_stopping()`);
2911
+ const stopped = this.B.tmp();
2912
+ this.B.line(`${stopped} = call zeroext i1 @scr_context_stopping()`);
2913
+ this.B.condBr(stopped, stop, ordinary);
2914
+ this.B.startBlock(stop);
2915
+ }
2536
2916
  const handlers = this.tryStack;
2537
2917
  this.tryStack = [];
2538
2918
  this.emitUnwind();
@@ -2574,6 +2954,11 @@ export class LlEmitter {
2574
2954
  }
2575
2955
  }
2576
2956
  if (entries.length === 0) {
2957
+ if (passThrough !== undefined && !target && this.currentWasiCoro === null) {
2958
+ const t = this.llType(this.currentReturnType);
2959
+ if (t === "double" || t === "i1")
2960
+ terminator = `ret ${t} ${passThrough}`;
2961
+ }
2577
2962
  this.B.terminate(terminator);
2578
2963
  return;
2579
2964
  }
@@ -2584,27 +2969,250 @@ export class LlEmitter {
2584
2969
  }
2585
2970
  this.B.br(cleanup.label);
2586
2971
  }
2972
+ /** Loads the running thread's exact pending word (ELF worker
2973
+ * executables, scr_exception.c): bit 0 mirrors the active cell, bit 1
2974
+ * requests the context's stop. It lives in the executable's static TLS
2975
+ * block, so the access is one thread-pointer-relative load. Monotonic by
2976
+ * default: a loop must not hoist the load out and miss a cross-thread
2977
+ * stop (see emitPendingCheck for the unordered tests). */
2978
+ loadExcFlag(unordered = false) {
2979
+ this.declare(`@scr_exc_flag = external thread_local(localexec) global i32, align 4`);
2980
+ const word = this.B.tmp();
2981
+ this.B.line(`${word} = load atomic i32, ptr @scr_exc_flag ${unordered ? "unordered" : "monotonic"}, align 4`);
2982
+ return word;
2983
+ }
2984
+ /** Instructions (two-space indented) that set the i1 `dest` to "an
2985
+ * exception is pending in the active context" — scr_exc_pending()'s
2986
+ * meaning — for the helper and adapter bodies that test it outside
2987
+ * emitPendingCheck. Executables read the runtime's exported active-cell
2988
+ * pointer and compare its i32 kind (offset 0) against SCR_EXC_NONE inline,
2989
+ * as emitPendingCheck does; the out-of-line call would be opaque to LLVM
2990
+ * because the runtime links as prebuilt objects. Libraries and worker
2991
+ * programs keep the runtime call, which is emitPendingCheck's path there
2992
+ * too (workers also observe cross-thread termination in it). */
2993
+ pendingTestLines(dest) {
2994
+ if (this.mod.lib !== undefined || this.mod.workers === true) {
2995
+ this.declare(`declare zeroext i1 @scr_exc_pending()`);
2996
+ return [` ${dest} = call zeroext i1 @scr_exc_pending()`];
2997
+ }
2998
+ this.declare(`@scr_exc_active = external global ptr`);
2999
+ return [
3000
+ ` ${dest}.cell = load ptr, ptr @scr_exc_active`,
3001
+ ` ${dest}.kind = load i32, ptr ${dest}.cell`,
3002
+ ` ${dest} = icmp ne i32 ${dest}.kind, 0`,
3003
+ ];
3004
+ }
2587
3005
  /** The emitter contract for exceptions: after EVERY call that can throw
2588
3006
  * (per the may-throw analysis), test the pending flag and unwind. The
2589
3007
  * call's result temp must join its frame BEFORE this runs so the unwind
2590
- * releases the dummy (NULL for refcounted kinds) harmlessly. */
2591
- emitPendingCheck() {
3008
+ * releases the dummy (NULL for refcounted kinds) harmlessly. A scalar
3009
+ * call result of the function's return type may be passed as the unwind's
3010
+ * dummy value (see emitUnwind). `poll` marks a worker termination poll
3011
+ * (function entry or loop budget), whose test must stay ordered. */
3012
+ emitPendingCheck(passThrough, poll = false) {
3013
+ const through = passThrough !== undefined &&
3014
+ this.llType(passThrough.type) === this.llType(this.currentReturnType) &&
3015
+ (passThrough.type.kind === "f64" || passThrough.type.kind === "bool")
3016
+ ? passThrough.name
3017
+ : undefined;
2592
3018
  const B = this.B;
2593
3019
  if (B.isTerminated())
2594
3020
  return;
2595
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
2596
- const p = B.tmp();
2597
- B.line(`${p} = call zeroext i1 @scr_exc_pending()`);
3021
+ if (this.mod.lib !== undefined) {
3022
+ this.declare(`declare zeroext i1 @scr_exc_pending()`);
3023
+ const p = B.tmp();
3024
+ B.line(`${p} = call zeroext i1 @scr_exc_pending()`);
3025
+ const lu = B.newLabel("exc.u");
3026
+ const lk = B.newLabel("exc.k");
3027
+ B.condBr(p, lu, lk);
3028
+ B.startBlock(lu);
3029
+ this.emitUnwind(through);
3030
+ B.startBlock(lk);
3031
+ return;
3032
+ }
3033
+ // Executables test the active exception cell's kind inline (its first
3034
+ // field). ELF worker executables test the thread's exact pending word,
3035
+ // which is also nonzero once the context must stop, and unwind directly:
3036
+ // no site carries an out-of-line call, so LLVM's inliner sees the check
3037
+ // as two instructions. Other worker programs test the process-wide
3038
+ // alert word: it is nonzero while any script thread may have an
3039
+ // exception pending in its active cell or has been asked to stop, so
3040
+ // the common case is one ordinary global load (a thread-local access is
3041
+ // a call on Darwin) and the out-of-line scr_exc_pending answers for this
3042
+ // thread, observing cancellation and reinstalling the termination
3043
+ // sentinel.
3044
+ this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
3045
+ let word;
3046
+ const exact = this.mod.workers !== true || this.executableTls;
3047
+ if (this.executableTls) {
3048
+ // Termination stays observable where the polls were dropped for it:
3049
+ // polls, tests inside source loops (exc's loop-poll rule relies on
3050
+ // them every iteration) and tail-position tests (a dead ordered load
3051
+ // keeps recursion from becoming a loop without stack growth) use an
3052
+ // ordered load, which LLVM never deletes, hoists or merges. Every
3053
+ // other test is an unordered load: it reads the same word, but LLVM's
3054
+ // memory analyses no longer treat it as a write, so loads around it
3055
+ // stay forwardable after inlining. Same-thread changes only happen in
3056
+ // runtime calls, which clobber memory.
3057
+ word = this.loadExcFlag(!poll && through === undefined && this.loopDepth === 0);
3058
+ }
3059
+ else if (this.mod.workers === true) {
3060
+ // dso_local: executables link the runtime statically, so the word is
3061
+ // addressed directly instead of through the GOT.
3062
+ this.declare(`@scr_exc_alert = external dso_local global i32, align 64`);
3063
+ word = B.tmp();
3064
+ B.line(`${word} = load atomic i32, ptr @scr_exc_alert monotonic, align 64`);
3065
+ }
3066
+ else {
3067
+ this.declare(`@scr_exc_active = external global ptr`);
3068
+ let cell;
3069
+ if (B.excCellInvariant) {
3070
+ // Fiber switches restore the active cell before control returns to a
3071
+ // synchronous frame, so its pointer is one value for the whole call:
3072
+ // load it once in the entry block and test only the kind here.
3073
+ if (B.excCell === null) {
3074
+ B.excCell = "%exc.cell";
3075
+ B.entryAllocas.push(`${B.excCell} = load ptr, ptr @scr_exc_active`);
3076
+ }
3077
+ cell = B.excCell;
3078
+ }
3079
+ else {
3080
+ cell = B.tmp();
3081
+ B.line(`${cell} = load ptr, ptr @scr_exc_active`);
3082
+ }
3083
+ word = B.tmp();
3084
+ B.line(`${word} = load i32, ptr ${cell}`);
3085
+ }
3086
+ const hit = B.tmp();
3087
+ const cold = B.tmp();
3088
+ B.line(`${hit} = icmp ne i32 ${word}, 0`);
3089
+ B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${hit}, i1 false)`);
2598
3090
  const lu = B.newLabel("exc.u");
2599
3091
  const lk = B.newLabel("exc.k");
2600
- B.condBr(p, lu, lk);
3092
+ if (!exact) {
3093
+ this.declare(`declare zeroext i1 @scr_exc_pending()`);
3094
+ const slow = B.newLabel("exc.s");
3095
+ B.condBr(cold, slow, lk);
3096
+ B.startBlock(slow);
3097
+ const p = B.tmp();
3098
+ B.line(`${p} = call zeroext i1 @scr_exc_pending()`);
3099
+ B.condBr(p, lu, lk);
3100
+ }
3101
+ else {
3102
+ B.condBr(cold, lu, lk);
3103
+ }
2601
3104
  B.startBlock(lu);
2602
- this.emitUnwind();
3105
+ this.emitUnwind(through);
2603
3106
  B.startBlock(lk);
2604
3107
  }
2605
- workerLoopBudget() {
3108
+ /** After a runtime call that ALWAYS leaves an exception pending (the
3109
+ * compiler-resolved Node throws): the pending test would always answer
3110
+ * true, so unwind unconditionally. Emission continues in a block with no
3111
+ * predecessors so the caller's remaining instructions stay well-formed;
3112
+ * LLVM deletes it. */
3113
+ emitAlwaysPendingUnwind() {
3114
+ const B = this.B;
3115
+ if (B.isTerminated())
3116
+ return;
3117
+ this.emitUnwind();
3118
+ B.startBlock(B.newLabel("exc.dead"));
3119
+ }
3120
+ /** A recursive function's entry check: below the running stack's guard,
3121
+ * throw Node's RangeError and unwind like any other pending exception. */
3122
+ emitStackCheck() {
3123
+ const B = this.B;
3124
+ this.declare(`declare ptr @llvm.stacksave.p0()`);
3125
+ this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
3126
+ this.declare(`declare void @scr_stack_overflow()`);
3127
+ const over = B.newLabel("stack.over");
3128
+ const ok = B.newLabel("stack.ok");
3129
+ const sp = B.tmp();
3130
+ B.line(`${sp} = call ptr @llvm.stacksave.p0()`);
3131
+ const test = (guard) => {
3132
+ const low = B.tmp();
3133
+ const cold = B.tmp();
3134
+ B.line(`${low} = icmp ult ptr ${sp}, ${guard}`);
3135
+ B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${low}, i1 false)`);
3136
+ B.condBr(cold, over, ok);
3137
+ };
3138
+ if (this.stackGuardTsd !== null) {
3139
+ // A Darwin thread-local access is a call to its TLV thunk: read the
3140
+ // guard from its thread-specific-data slot at the runtime-validated
3141
+ // offset instead, and call out only when the runtime found no slot.
3142
+ this.declare(`@scr_stack_guard_tsd = external dso_local global i64`);
3143
+ this.declare(`declare i64 @scr_stack_guard_get()`);
3144
+ const offset = B.tmp();
3145
+ const direct = B.tmp();
3146
+ const likely = B.tmp();
3147
+ B.line(`${offset} = load i64, ptr @scr_stack_guard_tsd`);
3148
+ B.line(`${direct} = icmp ne i64 ${offset}, 0`);
3149
+ B.line(`${likely} = call i1 @llvm.expect.i1(i1 ${direct}, i1 true)`);
3150
+ const fast = B.newLabel("stack.tsd");
3151
+ const slow = B.newLabel("stack.tlv");
3152
+ B.condBr(likely, fast, slow);
3153
+ B.startBlock(fast);
3154
+ const guard = B.tmp();
3155
+ if (this.stackGuardTsd === "aarch64") {
3156
+ this.declare(`declare i64 @llvm.read_register.i64(metadata)`);
3157
+ const thread = B.tmp();
3158
+ const base = B.tmp();
3159
+ const slot = B.tmp();
3160
+ const address = B.tmp();
3161
+ B.line(`${thread} = call i64 @llvm.read_register.i64(metadata !{!"tpidrro_el0"})`);
3162
+ B.line(`${base} = and i64 ${thread}, -8`);
3163
+ B.line(`${slot} = add i64 ${base}, ${offset}`);
3164
+ B.line(`${address} = inttoptr i64 ${slot} to ptr`);
3165
+ B.line(`${guard} = load ptr, ptr ${address}`);
3166
+ }
3167
+ else {
3168
+ const address = B.tmp();
3169
+ B.line(`${address} = inttoptr i64 ${offset} to ptr addrspace(256)`);
3170
+ B.line(`${guard} = load ptr, ptr addrspace(256) ${address}`);
3171
+ }
3172
+ test(guard);
3173
+ B.startBlock(slow);
3174
+ // uintptr_t scr_stack_guard_get(void): an i64 in the C ABI.
3175
+ const raw = B.tmp();
3176
+ const fallback = B.tmp();
3177
+ B.line(`${raw} = call i64 @scr_stack_guard_get()`);
3178
+ B.line(`${fallback} = inttoptr i64 ${raw} to ptr`);
3179
+ test(fallback);
3180
+ }
3181
+ else {
3182
+ const tl = this.mod.workers === true ? "thread_local " : "";
3183
+ this.declare(`@scr_stack_guard = external ${tl}global ptr`);
3184
+ const guard = B.tmp();
3185
+ B.line(`${guard} = load ptr, ptr @scr_stack_guard`);
3186
+ test(guard);
3187
+ }
3188
+ B.startBlock(over);
3189
+ B.line(`call void @scr_stack_overflow()`);
3190
+ this.emitUnwind();
3191
+ B.startBlock(ok);
3192
+ }
3193
+ /** Whether the emitter follows this call with a pending test (the
3194
+ * conditions in expr-calls.ts). */
3195
+ callTestsPending(e) {
3196
+ switch (e.kind) {
3197
+ case "call":
3198
+ return this.mayThrow.has(e.callee);
3199
+ case "new":
3200
+ return this.mayThrow.has(`%${e.className}.constructor`);
3201
+ case "virtualCall":
3202
+ return this.mayThrowMethods.has(e.method);
3203
+ case "callValue":
3204
+ return this.indirectMayThrow;
3205
+ default:
3206
+ return false;
3207
+ }
3208
+ }
3209
+ workerLoopBudget(loop) {
2606
3210
  if (!this.mod.workers)
2607
3211
  return null;
3212
+ // A loop that runs a pending test on every iteration observes a
3213
+ // terminate request through it; a separate poll would only add work.
3214
+ if (loopTestsEveryIteration(loop, (e) => this.callTestsPending(e)))
3215
+ return null;
2608
3216
  const slot = this.B.slot();
2609
3217
  this.B.entryAllocas.push(`${slot} = alloca i32 ; worker cancellation budget`);
2610
3218
  this.B.line(`store i32 0, ptr ${slot}`);
@@ -2626,7 +3234,7 @@ export class LlEmitter {
2626
3234
  // Calls and throwing operations retain their own checks. Amortize the
2627
3235
  // extra cancellation poll across bounded loop batches so scalar work
2628
3236
  // does not cross the runtime ABI on every iteration.
2629
- this.emitPendingCheck();
3237
+ this.emitPendingCheck(undefined, true);
2630
3238
  B.br(next);
2631
3239
  B.startBlock(next);
2632
3240
  }
@@ -2699,6 +3307,11 @@ export class LlEmitter {
2699
3307
  const def = this.unionsById.get(v.type.unionId);
2700
3308
  if (!def)
2701
3309
  throw new InternalCompilerError(`llvm emitter bug: truthiness of unknown union ${v.type.unionId}`);
3310
+ const nullable = this.nullableUnions.get(def.id);
3311
+ // Object arms are always truthy: present iff neither unit encoding.
3312
+ if (nullable && isObjectArm(nullable))
3313
+ return emitNullablePresent(B, nullable, v.name);
3314
+ const unionId = def.id;
2702
3315
  const slot = B.slot();
2703
3316
  B.entryAllocas.push(`${slot} = alloca i1`);
2704
3317
  const join = B.newLabel("ut.j");
@@ -2711,10 +3324,10 @@ export class LlEmitter {
2711
3324
  valueName = this.unionGetBool(v.name);
2712
3325
  }
2713
3326
  else if (arm.kind === "string") {
2714
- valueName = this.unionPeek(v.name);
3327
+ valueName = this.unionPeek(v.name, unionId);
2715
3328
  }
2716
3329
  else if (arm.kind === "bigint" || arm.kind === "dyn" || arm.kind === "jsval") {
2717
- valueName = this.unionPeek(v.name);
3330
+ valueName = this.unionPeek(v.name, unionId);
2718
3331
  }
2719
3332
  const truthy = this.truthyOf(arm.kind, valueName, true);
2720
3333
  B.line(`store i1 ${truthy}, ptr ${slot}`);
@@ -2820,22 +3433,22 @@ export class LlEmitter {
2820
3433
  }
2821
3434
  }
2822
3435
  // ── union plumbing ──────────────────────────────────────────────────────
2823
- /** Loads a union box's tag (i32). */
2824
- unionTag(uName) {
2825
- const p = this.B.tmp();
2826
- const t = this.B.tmp();
2827
- this.B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${uName}, i64 0, i32 1`);
2828
- this.B.line(`${t} = load i32, ptr ${p}`);
2829
- return t;
3436
+ /** A union value's tag (i32): the box's tag word, or the NULL test of a
3437
+ * nullable union's pointer. */
3438
+ unionTag(uName, unionId) {
3439
+ return emitUnionTag(this.B, this.nullableUnions.get(unionId), uName);
3440
+ }
3441
+ /** Guard for helpers that build or read a union's `%ScrUnion` box
3442
+ * directly (library-shaped unions with fixed arms): a nullable union
3443
+ * reaching one is an emitter bug, never a silent miscompile. */
3444
+ requireBoxedUnion(unionId) {
3445
+ if (this.nullableUnions.has(unionId))
3446
+ throw new InternalCompilerError(`llvm emitter bug: nullable union ${unionId} reached a tagged-box helper`);
2830
3447
  }
2831
3448
  /** The BORROWED payload pointer of a ref arm (scr_union_peek inlined —
2832
- * the runtime's is a static inline). */
2833
- unionPeek(uName) {
2834
- const p = this.B.tmp();
2835
- const t = this.B.tmp();
2836
- this.B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${uName}, i64 0, i32 5`);
2837
- this.B.line(`${t} = load ptr, ptr ${p}`);
2838
- return t;
3449
+ * the runtime's is a static inline); a nullable union is its payload. */
3450
+ unionPeek(uName, unionId) {
3451
+ return emitUnionPeek(this.B, this.nullableUnions.get(unionId), uName);
2839
3452
  }
2840
3453
  /** Emits `switch` over a union's tag with one block per arm; each arm
2841
3454
  * body must TERMINATE its block (the callers branch to a join). The
@@ -2843,7 +3456,7 @@ export class LlEmitter {
2843
3456
  * reads can group equivalent storage prefixes and emit one representative. */
2844
3457
  unionTagSwitch(uName, def, arm, fieldGroups) {
2845
3458
  const B = this.B;
2846
- const tag = this.unionTag(uName);
3459
+ const tag = this.unionTag(uName, def.id);
2847
3460
  const bad = B.newLabel("u.bad");
2848
3461
  const labels = [];
2849
3462
  if (fieldGroups) {
@@ -2889,12 +3502,12 @@ export class LlEmitter {
2889
3502
  /** The +1 extraction of a union's single narrowed arm (unionNarrow /
2890
3503
  * the nullish-family reads): scalars via inline payload loads, ref arms
2891
3504
  * a retained peek. */
2892
- unionExtract(uName, arm) {
3505
+ unionExtract(uName, unionId, arm) {
2893
3506
  if (arm.kind === "f64" || arm.kind === "procStream")
2894
3507
  return this.unionGetF64(uName);
2895
3508
  if (arm.kind === "bool")
2896
3509
  return this.unionGetBool(uName);
2897
- return this.retainValue(this.unionPeek(uName), arm);
3510
+ return this.retainValue(this.unionPeek(uName, unionId), arm);
2898
3511
  }
2899
3512
  /** A scalar f64 arm's payload (scr_union_get_f64 inlined). Inline loads
2900
3513
  * keep stack boxes free of escaping calls, so LLVM can scalarize them. */
@@ -2918,8 +3531,15 @@ export class LlEmitter {
2918
3531
  /** Constructs a union box around an OWNED (+1, already moved) value —
2919
3532
  * the scr_union_new_* dispatch of unionWrap and the wrap-into-join
2920
3533
  * sites (shift). */
2921
- unionNewOwned(tag, v) {
3534
+ unionNewOwned(unionId, tag, v) {
2922
3535
  const B = this.B;
3536
+ const nullable = this.nullableUnions.get(unionId);
3537
+ if (nullable) {
3538
+ // The owned payload IS the nullable union's value.
3539
+ if (tag !== nullable.refTag)
3540
+ throw new InternalCompilerError(`llvm emitter bug: payload wrap into unit arm ${tag} of ${unionId}`);
3541
+ return v.name;
3542
+ }
2923
3543
  const t = B.tmp();
2924
3544
  if (v.type.kind === "f64" || v.type.kind === "procStream") {
2925
3545
  this.declare(`declare ptr @scr_union_new_f64(i32, double)`);
@@ -3062,6 +3682,42 @@ export class LlEmitter {
3062
3682
  B.line(`${z} = zext i1 ${value} to i8`);
3063
3683
  B.line(`store i8 ${z}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
3064
3684
  }
3685
+ /** Reads an int32-specialized class field: the i32 slot is the value,
3686
+ * and its widening is the exact double every program observation sees. */
3687
+ loadInt32Field(ptr, type) {
3688
+ const B = this.B;
3689
+ const raw = B.tmp();
3690
+ const number = B.tmp();
3691
+ B.line(`${raw} = load i32, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
3692
+ B.line(`${number} = sitofp i32 ${raw} to double`);
3693
+ return {
3694
+ name: number,
3695
+ type,
3696
+ uint32: raw,
3697
+ integer: { name: raw, type: "i32", signed: true, range: INT32_RANGE },
3698
+ };
3699
+ }
3700
+ /** Stores a value the whole-program analysis proved to be an int32 (never
3701
+ * -0, NaN or fractional) into an int32-specialized class field. */
3702
+ storeInt32Field(ptr, value, expr) {
3703
+ if (!this.int32Slots.provenWrites.has(expr))
3704
+ throw new InternalCompilerError("llvm emitter bug: unproven int32 field write");
3705
+ const B = this.B;
3706
+ let bits;
3707
+ if (value.integer?.type === "i32")
3708
+ bits = value.integer.name;
3709
+ else if (value.integer) {
3710
+ bits = B.tmp();
3711
+ B.line(`${bits} = trunc i64 ${value.integer.name} to i32`);
3712
+ }
3713
+ else if (value.uint32 !== undefined)
3714
+ bits = value.uint32;
3715
+ else {
3716
+ bits = B.tmp();
3717
+ B.line(`${bits} = fptosi double ${value.name} to i32`);
3718
+ }
3719
+ B.line(`store i32 ${bits}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
3720
+ }
3065
3721
  // ── bindings ────────────────────────────────────────────────────────────
3066
3722
  /** A binding's storage: a module global, a plain local slot, or a boxed
3067
3723
  * local (the slot holds the capture BOX; access goes through it). */
@@ -3182,18 +3838,38 @@ export class LlEmitter {
3182
3838
  this.declare(`declare void @scr_box_set_ref(ptr, ptr)`);
3183
3839
  B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${cell})`);
3184
3840
  }
3841
+ /** The payload a TDZ box holds before its declaration runs, when that is
3842
+ * not NULL. A nullable union's value is its arm pointer with NULL as a
3843
+ * unit arm (nullable-unions.ts), so a stored `null`/`undefined` would
3844
+ * read back as the temporal dead zone; such boxes start at the module's
3845
+ * immortal NULLABLE_ABSENT sentinel instead, which no value of the
3846
+ * binding can hold and every RC entry point and the collector skip. */
3847
+ tdzEmptySentinel(t) {
3848
+ if (!this.nullableUnions.of(t))
3849
+ return null;
3850
+ this.needsNullableAbsent = true;
3851
+ this.immortalValues.add(NULLABLE_ABSENT);
3852
+ return NULLABLE_ABSENT;
3853
+ }
3185
3854
  /** The TDZ-guarded read of a boxed binding: an empty payload slot is
3186
3855
  * the temporal dead zone — throw Node's exact catchable ReferenceError
3187
3856
  * (exprs.ts's varRef guard). Scalars then peek the one-element
3188
3857
  * array cell; ref kinds read the box normally (+1). */
3189
- checkTdz(box, name) {
3858
+ checkTdz(box, t, name) {
3190
3859
  const B = this.B;
3191
3860
  const slotp = B.tmp();
3192
3861
  const slotv = B.tmp();
3193
3862
  const empty = B.tmp();
3194
3863
  B.line(`${slotp} = getelementptr inbounds %ScrBox, ptr ${box}, i64 0, i32 5`);
3195
- B.line(`${slotv} = load i64, ptr ${slotp}`);
3196
- B.line(`${empty} = icmp eq i64 ${slotv}, 0`);
3864
+ const sentinel = this.tdzEmptySentinel(t);
3865
+ if (sentinel !== null) {
3866
+ B.line(`${slotv} = load ptr, ptr ${slotp}`);
3867
+ B.line(`${empty} = icmp eq ptr ${slotv}, ${sentinel}`);
3868
+ }
3869
+ else {
3870
+ B.line(`${slotv} = load i64, ptr ${slotp}`);
3871
+ B.line(`${empty} = icmp eq i64 ${slotv}, 0`);
3872
+ }
3197
3873
  this.throwIfUninitialized(empty, name);
3198
3874
  return slotv;
3199
3875
  }
@@ -3230,7 +3906,7 @@ export class LlEmitter {
3230
3906
  B.startBlock(lk);
3231
3907
  }
3232
3908
  tdzBoxRead(box, t, name) {
3233
- const slotv = this.checkTdz(box, name);
3909
+ const slotv = this.checkTdz(box, t, name);
3234
3910
  const B = this.B;
3235
3911
  const acc = boxAccess(t);
3236
3912
  if (acc === "ref")
@@ -3250,7 +3926,7 @@ export class LlEmitter {
3250
3926
  writeBindingBox(box, local, value, initializes = false, borrowed = false) {
3251
3927
  if (local.tdz) {
3252
3928
  const first = initializes || !local.mutable;
3253
- const slotv = first ? null : this.checkTdz(box, local.name);
3929
+ const slotv = first ? null : this.checkTdz(box, local.type, local.name);
3254
3930
  const acc = boxAccess(local.type);
3255
3931
  if (acc !== "ref") {
3256
3932
  if (first)
@@ -3375,6 +4051,9 @@ export class LlEmitter {
3375
4051
  emitFunction(fn) {
3376
4052
  const B = new BlockBuilder();
3377
4053
  this.B = B;
4054
+ // Fiber switches restore the active exception cell before control
4055
+ // returns to a synchronous frame (emitPendingCheck).
4056
+ B.excCellInvariant = fn.async !== true && fn.generator === undefined;
3378
4057
  this.debugScope = this.debug?.function(fn) ?? null;
3379
4058
  B.debugLocation = this.debug?.location(fn.loc, this.debugScope) ?? null;
3380
4059
  this.frames = [];
@@ -3407,6 +4086,7 @@ export class LlEmitter {
3407
4086
  const numericFn = withInitializerBindings(fn, initializerBindings);
3408
4087
  this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
3409
4088
  this.borrowedParameters.clear();
4089
+ this.reboundOwnerSlots.clear();
3410
4090
  this.projectedParameters.clear();
3411
4091
  const projectedParameterIndexes = this.callLifetimes.parameters.get(fn.name);
3412
4092
  if (projectedParameterIndexes)
@@ -3415,12 +4095,18 @@ export class LlEmitter {
3415
4095
  this.stableCallBindings = this.callLifetimes.bindings.get(fn.name) ?? new Set();
3416
4096
  const borrowedParameterIndexes = this.callLifetimes.borrowed.get(fn.name);
3417
4097
  if (borrowedParameterIndexes) {
4098
+ // Rebound walk parameters borrow too, but only unchanged ones may
4099
+ // serve as stable owners for call arguments and aliases.
3418
4100
  for (const index of borrowedParameterIndexes)
3419
- this.borrowedParameters.add(fn.params[index].localId);
4101
+ if (!this.walkBorrowsByFunction.get(fn.name)?.has(fn.params[index].localId) &&
4102
+ !this.reboundByFunction.get(fn.name)?.has(fn.params[index].localId))
4103
+ this.borrowedParameters.add(fn.params[index].localId);
3420
4104
  }
3421
4105
  this.captureIds = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId));
3422
4106
  this.integerLoopBindings.clear();
3423
4107
  this.countedLoopsEnabled = this.debug === null && !fn.async && !fn.generator;
4108
+ this.finalFieldBorrows =
4109
+ !fn.async && !fn.generator && constructorClass(fn.name) === null && this.finalFields.size > 0;
3424
4110
  this.byteWindowsEnabled = this.countedLoopsEnabled;
3425
4111
  this.byteWindowEntries =
3426
4112
  this.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
@@ -3433,21 +4119,24 @@ export class LlEmitter {
3433
4119
  this.debug === null
3434
4120
  ? findLoopArrayBorrows(fn, this.callLifetimes, this.referenceEffects, this.optionalArrayReads)
3435
4121
  : new Set();
3436
- this.localArrayReads = findLocalArrayReads(fn, this.fnByName, this.unionsById, this.referenceEffects.functions, this.callLifetimes, this.optionalArrayReads, this.loopArrayBorrows);
3437
- this.callArrayReads = findCallArrayReads(fn, this.optionalArrayReads, this.referenceEffects.functions, this.callLifetimes);
4122
+ this.localArrayReads = findLocalArrayReads(fn, this.fnByName, this.unionsById, this.referenceEffects.functions, this.callLifetimes, this.optionalArrayReads, this.loopArrayBorrows, this.debug === null ? this.referenceEffects : undefined);
4123
+ this.callArrayReads = findCallArrayReads(fn, this.optionalArrayReads, this.referenceEffects.functions, this.callLifetimes, this.debug === null ? this.referenceEffects : undefined);
3438
4124
  this.scalarStringSlices =
3439
4125
  this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
3440
- this.mapReadLifetimes = findMapReadLifetimes(fn, this.unionsById, this.callLifetimes);
3441
- this.localStackUnions = findLocalStackUnions(fn, this.callLifetimes, this.unionsById);
3442
- this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.unionsById, (e) => this.nullableFieldGet(e) !== null);
4126
+ this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
4127
+ this.localStackUnions = findLocalStackUnions(fn, this.callLifetimes, this.boxedUnionsById);
4128
+ this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.boxedUnionsById, (e) => this.nullableFieldGet(e) !== null);
3443
4129
  this.localUnionStorage = new Map();
3444
- this.integerRanges = analyzeIntegerRanges(numericFn);
4130
+ this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set();
4131
+ this.integerRanges = analyzeIntegerRanges(numericFn, this.int32Slots.facts(fn.name));
3445
4132
  this.bytesBounds = findBytesBounds(numericFn, this.integerRanges);
3446
4133
  this.chainSlots.clear();
3447
4134
  this.finallyStack = [];
3448
4135
  this.tryStack = [];
4136
+ this.loopDepth = 0;
3449
4137
  this.currentReturnType = fn.returnType;
3450
4138
  this.currentGenerator = fn.generator ?? null;
4139
+ this.currentBorrowedReturn = this.borrowedReturns.has(fn.name);
3451
4140
  this.currentWasiCoro = null;
3452
4141
  this.logArgSlots = 0;
3453
4142
  if (this.wasi && (fn.async === true || fn.generator !== undefined)) {
@@ -3551,7 +4240,12 @@ export class LlEmitter {
3551
4240
  const cache = B.slot();
3552
4241
  B.entryAllocas.push(`${cache} = alloca ptr ; lazy box ${this.currentLocals.get(id).name}`);
3553
4242
  B.line(`store ptr null, ptr ${cache}`);
3554
- fnScope.push({ slot: cache, type: this.currentLocals.get(id).type, boxed: true });
4243
+ fnScope.push({
4244
+ slot: cache,
4245
+ type: this.currentLocals.get(id).type,
4246
+ boxed: true,
4247
+ lazy: true,
4248
+ });
3555
4249
  this.lazyCaptureBoxes.set(id, cache);
3556
4250
  // Environments that finish during the call can share an immortal
3557
4251
  // frame box. Its payload owner is released with the function scope.
@@ -3578,6 +4272,15 @@ export class LlEmitter {
3578
4272
  this.storeIntegerView(p.localId, { name: `%p_${mangleLocal(p.localId)}`, type: p.type });
3579
4273
  if (isRefCounted(p.type) && !borrowed?.has(index))
3580
4274
  fnScope.push({ slot, type: p.type });
4275
+ if (this.reboundByFunction.get(fn.name)?.has(p.localId) && borrowed?.has(index)) {
4276
+ // The caller's borrow stays in the parameter slot; values the body
4277
+ // assigns are owned here until rebinding or exit releases them.
4278
+ const owner = `${slot}.owner`;
4279
+ B.entryAllocas.push(`${owner} = alloca ptr`);
4280
+ B.line(`store ptr null, ptr ${owner}`);
4281
+ fnScope.push({ slot: owner, type: p.type });
4282
+ this.reboundOwnerSlots.set(p.localId, owner);
4283
+ }
3581
4284
  }
3582
4285
  this.scopes.push(fnScope);
3583
4286
  // Stackful native fibers retain these frames while suspended. The
@@ -3590,8 +4293,10 @@ export class LlEmitter {
3590
4293
  B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${this.cstr(frame)})`);
3591
4294
  B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
3592
4295
  }
3593
- if (this.mod.workers)
3594
- this.emitPendingCheck();
4296
+ if (this.stackChecks?.has(fn.name))
4297
+ this.emitStackCheck();
4298
+ if (this.workerEntryPolls?.has(fn.name))
4299
+ this.emitPendingCheck(undefined, true);
3595
4300
  this.emitStmts(fn.body);
3596
4301
  // Implicit exit of a void function: release the function scope unless
3597
4302
  // the body already terminated its final block (return, or a throw
@@ -3662,24 +4367,55 @@ export class LlEmitter {
3662
4367
  const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`;
3663
4368
  const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
3664
4369
  const body = `define internal ${ret} @${symbol}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
3665
- return borrowed ? body + "\n" + this.emitOwnedCallAdapter(fn, borrowed) : body;
4370
+ if (!borrowed)
4371
+ return body;
4372
+ const slot = this.implSlotBorrowed(fn.name);
4373
+ const virtual = slot.size > 0 && slot.size !== borrowed.size
4374
+ ? "\n" + this.emitOwnedCallAdapter(fn, borrowed, slot)
4375
+ : "";
4376
+ return body + "\n" + this.emitOwnedCallAdapter(fn, borrowed) + virtual;
3666
4377
  }
3667
4378
  /** Keep the ordinary owned ABI for closures, virtual dispatch, generated
3668
4379
  * runtime adapters and library exports. Only direct IR calls select the
3669
4380
  * borrowing body. Ownership of other parameters moves into that body;
3670
4381
  * borrowed parameters are released here on normal and exceptional exits.
3671
4382
  * The pending exception remains for the adapter's caller to handle. */
3672
- emitOwnedCallAdapter(fn, borrowed) {
4383
+ /** With `kept`, the virtual adapter of a borrowing slot instead: the
4384
+ * slot's parameters stay borrowed, the body's other borrowed ones are
4385
+ * released here. */
4386
+ emitOwnedCallAdapter(fn, borrowed, kept = NO_BORROWED) {
3673
4387
  const B = new BlockBuilder();
3674
4388
  this.B = B;
3675
4389
  const params = fn.params.map((param, index) => `${this.llType(param.type)} %p${index}`);
3676
4390
  const ret = this.llType(fn.returnType);
3677
4391
  const call = `call ${ret} @${mangleBorrowedFunction(fn.name)}(${params.join(", ")})`;
3678
4392
  B.line(ret === "void" ? call : `%result = ${call}`);
4393
+ // A borrowed result is reachable from the parameters: own it first. A
4394
+ // throwing body returns a null dummy, which needs no owner (and string
4395
+ // or array retains do not accept NULL).
4396
+ let result = "%result";
4397
+ if (this.borrowedReturns.has(fn.name)) {
4398
+ const slot = B.slot();
4399
+ B.entryAllocas.push(`${slot} = alloca ptr`);
4400
+ B.line(`store ptr null, ptr ${slot}`);
4401
+ const present = B.tmp();
4402
+ const own = B.newLabel("result.own");
4403
+ const done = B.newLabel("result.done");
4404
+ B.line(`${present} = icmp ne ptr %result, null`);
4405
+ B.condBr(present, own, done);
4406
+ B.startBlock(own);
4407
+ B.line(`store ptr ${this.retainValue("%result", fn.returnType)}, ptr ${slot}`);
4408
+ B.br(done);
4409
+ B.startBlock(done);
4410
+ result = B.tmp();
4411
+ B.line(`${result} = load ptr, ptr ${slot}`);
4412
+ }
3679
4413
  for (const index of borrowed)
3680
- this.releaseValue(`%p${index}`, fn.params[index].type);
3681
- B.terminate(ret === "void" ? "ret void" : `ret ${ret} %result`);
3682
- return `define internal ${ret} @${mangleFunction(fn.name)}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`;
4414
+ if (!kept.has(index))
4415
+ this.releaseValue(`%p${index}`, fn.params[index].type);
4416
+ B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`);
4417
+ const symbol = kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name);
4418
+ return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`;
3683
4419
  }
3684
4420
  /** A local stable binding keeps its value alive while later arguments
3685
4421
  * and the callee execute. A proven local union box cannot escape or be
@@ -3688,6 +4424,46 @@ export class LlEmitter {
3688
4424
  canBorrowCallArgument(value) {
3689
4425
  if (value.kind === "strLit")
3690
4426
  return true;
4427
+ // Class casts reinterpret the same pointer (prefix layouts).
4428
+ if ((value.kind === "upcast" || value.kind === "downcast") &&
4429
+ value.value.type.kind === "object")
4430
+ return this.canBorrowCallArgument(value.value);
4431
+ if (value.kind === "seqExpr") {
4432
+ const guarded = guardedValue(value);
4433
+ return guarded !== null && this.canBorrowCallArgument(guarded);
4434
+ }
4435
+ // A nullable-union wrap reinterprets its payload pointer; an immortal
4436
+ // unit constant needs no owner at all.
4437
+ const wrap = this.borrowableNullableWrap(value);
4438
+ if (wrap === "unit")
4439
+ return true;
4440
+ if (wrap === "ref" && value.kind === "unionWrap")
4441
+ return this.canBorrowCallArgument(value.value);
4442
+ // Checked casts (`x as C`, `x!` on a nullable) lower to a ternary whose
4443
+ // failing arm always throws; the successful arm projects the same
4444
+ // stable owner. The throwing arm's typed dummy owns nothing.
4445
+ if (value.kind === "unionNarrow")
4446
+ return this.canBorrowCallArgument(value.value);
4447
+ if (value.kind === "libCall")
4448
+ return value.fn === "error.nodeThrow";
4449
+ // A final field never changes outside constructors: while its object is
4450
+ // borrowable, so is the field's value. Nullable-pointer fields have no
4451
+ // box to lend and keep their owning path.
4452
+ if (value.kind === "fieldGet" &&
4453
+ this.finalFieldBorrows &&
4454
+ isRefCounted(value.type) &&
4455
+ this.finalFields.isFinal(value.className, value.field) &&
4456
+ !this.nullableFieldGet(value))
4457
+ return this.canBorrowReceiver(value.obj) && this.canBorrowCallArgument(value.obj);
4458
+ if (value.kind === "ternary")
4459
+ return (this.canBorrowReceiver(value.then) &&
4460
+ this.canBorrowReceiver(value.else_) &&
4461
+ this.canBorrowCallArgument(value.then) &&
4462
+ this.canBorrowCallArgument(value.else_));
4463
+ // An optional chain's bound receiver stays unchanged until its body
4464
+ // finishes (see emitReadReceiver).
4465
+ if (value.kind === "chainRecv")
4466
+ return this.chainSlots.has(value.id);
3691
4467
  if (value.kind !== "varRef")
3692
4468
  return false;
3693
4469
  const binding = this.binding(value.localId);
@@ -3729,11 +4505,16 @@ export class LlEmitter {
3729
4505
  emitStmt(s) {
3730
4506
  const previous = this.B.debugLocation;
3731
4507
  this.B.debugLocation = this.debug?.location(s.loc, this.debugScope) ?? null;
4508
+ const loop = s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf";
4509
+ if (loop)
4510
+ this.loopDepth++;
3732
4511
  try {
3733
4512
  this.emitStmtBody(s);
3734
4513
  }
3735
4514
  finally {
3736
4515
  this.B.debugLocation = previous;
4516
+ if (loop)
4517
+ this.loopDepth--;
3737
4518
  }
3738
4519
  }
3739
4520
  emitStmtBody(s) {
@@ -3742,6 +4523,12 @@ export class LlEmitter {
3742
4523
  switch (s.kind) {
3743
4524
  case "varDecl": {
3744
4525
  const b = this.binding(s.localId);
4526
+ if (this.walkBorrows.has(s.localId) && b.kind === "local") {
4527
+ // A borrowed walk pointer: no retain now, no release at scope exit.
4528
+ const v = s.init === null ? "null" : this.emitReadReceiver(s.init).name;
4529
+ B.line(`store ptr ${v}, ptr ${b.slot}`);
4530
+ break;
4531
+ }
3745
4532
  const slice = this.scalarStringSlices.get(s.localId);
3746
4533
  if (slice) {
3747
4534
  const snapshot = emitStringSliceSnapshot(this, slice);
@@ -3824,8 +4611,14 @@ export class LlEmitter {
3824
4611
  B.line(`${box} = ${boxNew} ; let ${b.local.name} (boxed)`);
3825
4612
  B.line(`store ptr ${box}, ptr ${b.slot}`);
3826
4613
  this.scopes[this.scopes.length - 1].push({ slot: b.slot, type: b.type, boxed: true });
3827
- if (s.init === null)
4614
+ if (s.init === null) {
4615
+ // An empty TDZ box whose payload can legitimately be NULL
4616
+ // starts at its distinct sentinel (tdzEmptySentinel).
4617
+ const sentinel = b.local.tdz === true ? this.tdzEmptySentinel(b.type) : null;
4618
+ if (sentinel !== null)
4619
+ B.line(`store ptr ${sentinel}, ptr ${this.boxSlot(box)}`);
3828
4620
  break;
4621
+ }
3829
4622
  const v = this.emitExpr(s.init);
3830
4623
  if (isRefCounted(v.type))
3831
4624
  this.moveTemp(v); // the box takes ownership
@@ -3874,6 +4667,23 @@ export class LlEmitter {
3874
4667
  break;
3875
4668
  }
3876
4669
  case "assign": {
4670
+ const owner = this.reboundOwnerSlots.get(s.localId);
4671
+ if (owner !== undefined && !this.walkBorrows.has(s.localId)) {
4672
+ // A rebound borrowed parameter: the new value moves into the owner
4673
+ // slot, replacing (and releasing) the previous owned value. This
4674
+ // precedes the in-place string append, which would release the
4675
+ // caller's borrow.
4676
+ const param = this.binding(s.localId);
4677
+ const slot = param.slot;
4678
+ const v = this.emitExpr(s.value);
4679
+ this.moveTemp(v);
4680
+ const old = B.tmp();
4681
+ B.line(`${old} = load ptr, ptr ${owner}`);
4682
+ this.releaseValue(old, param.type);
4683
+ B.line(`store ptr ${v.name}, ptr ${slot}`);
4684
+ B.line(`store ptr ${v.name}, ptr ${owner}`);
4685
+ break;
4686
+ }
3877
4687
  const localStorage = this.localUnionStorage.get(s.localId);
3878
4688
  if (localStorage) {
3879
4689
  storeLocalUnion(this, localStorage, s.value);
@@ -3886,6 +4696,11 @@ export class LlEmitter {
3886
4696
  break;
3887
4697
  }
3888
4698
  const b = this.binding(s.localId);
4699
+ if (this.walkBorrows.has(s.localId) && b.kind === "local") {
4700
+ // Rebinding a borrowed walk pointer releases nothing.
4701
+ B.line(`store ptr ${this.emitReadReceiver(s.value).name}, ptr ${b.slot}`);
4702
+ break;
4703
+ }
3889
4704
  const v = this.emitExpr(s.value);
3890
4705
  if (b.kind === "global" && !s.initializes)
3891
4706
  this.checkGlobalTdz(s.localId);
@@ -3907,7 +4722,9 @@ export class LlEmitter {
3907
4722
  break;
3908
4723
  }
3909
4724
  case "exprStmt":
3910
- this.emitExpr(s.expr);
4725
+ // A statement-position splice never observes its removed elements.
4726
+ if (!emitDiscardedSplice(this, s.expr))
4727
+ this.emitDiscarded(s.expr);
3911
4728
  break;
3912
4729
  case "arraySet": {
3913
4730
  // Evaluation order matches JS: array, index, then value. Ownership
@@ -3919,19 +4736,43 @@ export class LlEmitter {
3919
4736
  if (s.arr.type.kind !== "array")
3920
4737
  throw new InternalCompilerError("llvm emitter bug: arraySet on non-array");
3921
4738
  const acc = elemAccess(s.arr.type.elem);
4739
+ this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
3922
4740
  if (acc === "ref")
3923
4741
  this.moveTemp(v);
3924
4742
  emitDenseArraySet(this, arr.name, idx, s.index, acc, s.arr.type.elem, v.name);
3925
4743
  break;
3926
4744
  }
3927
4745
  case "arraySetLength": {
4746
+ // Pop's `length = 0` store on an empty array only matters when the
4747
+ // array can be frozen (published); its operand is a local.
4748
+ if (s.pop === true &&
4749
+ s.length.kind === "numLit" &&
4750
+ s.length.value === 0 &&
4751
+ this.publishedTypes?.containerGuarded(s.arr.type) !== true)
4752
+ break;
3928
4753
  const arr = emitBorrowedInput(this, s.arr);
3929
4754
  const length = this.emitExpr(s.length);
3930
4755
  if (s.arr.type.kind !== "array")
3931
4756
  throw new InternalCompilerError("llvm emitter bug: arraySetLength on non-array");
4757
+ this.emitPublishedArrayGuard(arr.name, s.arr.type, s.pop === true ? 2 : 3);
3932
4758
  this.declare(`declare void @scr_arr_set_len(ptr, double)`);
4759
+ // Assigning the current length (a reused work array reset to where
4760
+ // it already is) changes nothing: the runtime's validation accepts
4761
+ // exactly the value equal to the length (-0 included, NaN never),
4762
+ // and no element is released. Everything else keeps the call.
4763
+ const lenPtr = B.tmp(), len = B.tmp(), lenNumber = B.tmp(), same = B.tmp();
4764
+ const resize = B.newLabel("arr.len.set"), done = B.newLabel("arr.len.done");
4765
+ B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
4766
+ this.markMemoryPointer(lenPtr, "array:header");
4767
+ B.line(`${len} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`);
4768
+ B.line(`${lenNumber} = sitofp ${this.sizeType} ${len} to double`);
4769
+ B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`);
4770
+ B.condBr(same, done, resize);
4771
+ B.startBlock(resize);
3933
4772
  B.line(`call void @scr_arr_set_len(ptr ${arr.name}, double ${length.name})`);
3934
4773
  this.emitPendingCheck();
4774
+ B.br(done);
4775
+ B.startBlock(done);
3935
4776
  break;
3936
4777
  }
3937
4778
  case "arraySetUndefined":
@@ -3940,6 +4781,7 @@ export class LlEmitter {
3940
4781
  const idx = this.emitExpr(s.index);
3941
4782
  if (s.arr.type.kind !== "array")
3942
4783
  throw new InternalCompilerError(`llvm emitter bug: ${s.kind} on non-array`);
4784
+ this.emitPublishedArrayGuard(arr.name, s.arr.type, s.kind === "arrayDelete" ? 5 : 0, idx.name);
3943
4785
  const fn = s.kind === "arraySetUndefined" ? "scr_arr_set_undefined" : "scr_arr_delete";
3944
4786
  this.declare(`declare ${s.kind === "arrayDelete" ? "zeroext i1" : "void"} @${fn}(ptr, double)`);
3945
4787
  if (s.kind === "arrayDelete")
@@ -3971,14 +4813,27 @@ export class LlEmitter {
3971
4813
  // Classes and records share the struct layout, so one emission.
3972
4814
  const nullable = s.kind === "fieldSet" ? this.nullableFields.get(s.className, s.field) : null;
3973
4815
  if (nullable && s.kind === "fieldSet") {
3974
- const obj = this.emitExpr(s.obj);
4816
+ const obj = this.emitStableReceiver(s.obj, [s.value]);
4817
+ // (The guard precedes the value here: the nullable store
4818
+ // evaluates it itself.)
4819
+ this.emitPublishedFieldGuard(obj.name, s.className, s.field);
3975
4820
  this.emitNullableFieldStore(obj.name, s.className, s.field, nullable, s.value);
3976
4821
  break;
3977
4822
  }
3978
- const obj = isRefCounted(s.value.type)
3979
- ? this.emitExpr(s.obj)
3980
- : this.emitStableReceiver(s.obj, [s.value]);
4823
+ if (s.kind === "fieldSet" && this.int32Slots.isField(s.className, s.field)) {
4824
+ const obj = this.emitStableReceiver(s.obj, [s.value]);
4825
+ const v = this.emitExpr(s.value);
4826
+ this.emitPublishedFieldGuard(obj.name, s.className, s.field);
4827
+ const { ptr } = this.classFieldPtr(obj.name, s.className, s.field);
4828
+ this.storeInt32Field(ptr, v, s.value);
4829
+ break;
4830
+ }
4831
+ const obj = this.emitStableReceiver(s.obj, [s.value]);
3981
4832
  const v = this.emitExpr(s.value);
4833
+ if (s.kind === "fieldSet")
4834
+ this.emitPublishedFieldGuard(obj.name, s.className, s.field);
4835
+ else if (this.publishedTypes?.recordGuarded(s.shapeId) === true)
4836
+ this.emitPublishedFieldGuard(obj.name, null, s.field);
3982
4837
  const { ptr, type } = s.kind === "fieldSet"
3983
4838
  ? this.classFieldPtr(obj.name, s.className, s.field)
3984
4839
  : this.recordFieldPtr(obj.name, s.shapeId, s.field);
@@ -4012,6 +4867,11 @@ export class LlEmitter {
4012
4867
  // the overflow (key and value released; absent keys no-op).
4013
4868
  const obj = this.emitExpr(s.obj);
4014
4869
  const key = this.emitExpr(s.key);
4870
+ if (this.publishedTypes?.recordGuarded(s.shapeId) === true) {
4871
+ // Node: "Cannot delete property 'k' of #<Object>" (key known at
4872
+ // run time only; the guard reports the frozen record generically).
4873
+ this.emitPublishedFieldGuard(obj.name, null, "[key]");
4874
+ }
4015
4875
  const ovf = this.recordOvfPtr(obj.name, s.shapeId);
4016
4876
  this.declare(`declare zeroext i1 @scr_map_delete_str(ptr, ptr)`);
4017
4877
  const t = B.tmp();
@@ -4027,6 +4887,8 @@ export class LlEmitter {
4027
4887
  const obj = this.emitExpr(s.obj);
4028
4888
  const key = this.emitExpr(s.key);
4029
4889
  const v = this.emitExpr(s.value);
4890
+ if (this.publishedTypes?.recordGuarded(s.shapeId) === true)
4891
+ this.emitPublishedFieldGuard(obj.name, null, "[key]");
4030
4892
  if (isRefCounted(v.type))
4031
4893
  this.moveTemp(v);
4032
4894
  const shape = this.recordShape(s.shapeId);
@@ -4081,9 +4943,9 @@ export class LlEmitter {
4081
4943
  B.line(`call void @scr_dyn_release(ptr ${v.name})`);
4082
4944
  // Mismatched write: TypeError pending, field untouched — the
4083
4945
  // statement-level check below unwinds.
4084
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
4085
4946
  const pend = B.tmp();
4086
- B.line(`${pend} = call zeroext i1 @scr_exc_pending()`);
4947
+ for (const line of this.pendingTestLines(pend))
4948
+ B.line(line.trimStart());
4087
4949
  const lw = B.newLabel("rks.w");
4088
4950
  B.condBr(pend, join, lw);
4089
4951
  B.startBlock(lw);
@@ -4191,7 +5053,7 @@ export class LlEmitter {
4191
5053
  break;
4192
5054
  }
4193
5055
  case "while": {
4194
- const workerBudget = this.workerLoopBudget();
5056
+ const workerBudget = this.workerLoopBudget(s);
4195
5057
  const lc = B.newLabel("loop.c");
4196
5058
  const lb = B.newLabel("loop.b");
4197
5059
  const le = B.newLabel("loop.e");
@@ -4216,7 +5078,7 @@ export class LlEmitter {
4216
5078
  break;
4217
5079
  }
4218
5080
  case "doWhile": {
4219
- const workerBudget = this.workerLoopBudget();
5081
+ const workerBudget = this.workerLoopBudget(s);
4220
5082
  // Body first (runs at least once); continue jumps to the CONDITION.
4221
5083
  const lb = B.newLabel("loop.b");
4222
5084
  const lc = B.newLabel("loop.c");
@@ -4281,12 +5143,39 @@ export class LlEmitter {
4281
5143
  }
4282
5144
  let countedJoin = null;
4283
5145
  if (countedLoop?.guarded) {
4284
- const bound = this.emitExpr(countedLoop.limit);
4285
- const safe = B.tmp();
5146
+ let safe = "true";
5147
+ if (countedLoop.limitGuarded) {
5148
+ const bound = this.emitExpr(countedLoop.limit);
5149
+ safe = B.tmp();
5150
+ B.line(`${safe} = fcmp ${countedLoop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(countedLoop.guardLimit)}`);
5151
+ }
5152
+ if (countedLoop.startGuarded) {
5153
+ // An exact integer of at most 2^53 - 1 in magnitude, and not -0
5154
+ // (the bit patterns of the value and its round trip agree).
5155
+ // NaN and infinities fail the ordered range tests.
5156
+ const start = this.emitExpr(countedLoop.start);
5157
+ const low = B.tmp(), high = B.tmp(), raw = B.tmp(), wide = B.tmp(), back = B.tmp(), bits = B.tmp(), backBits = B.tmp(), exact = B.tmp(), inRange = B.tmp(), ok = B.tmp();
5158
+ B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`);
5159
+ B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
5160
+ B.line(`${inRange} = and i1 ${low}, ${high}`);
5161
+ B.line(`${raw} = fptosi double ${start.name} to i64`);
5162
+ B.line(`${wide} = freeze i64 ${raw}`);
5163
+ B.line(`${back} = sitofp i64 ${wide} to double`);
5164
+ B.line(`${bits} = bitcast double ${start.name} to i64`);
5165
+ B.line(`${backBits} = bitcast double ${back} to i64`);
5166
+ B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
5167
+ B.line(`${ok} = and i1 ${inRange}, ${exact}`);
5168
+ if (safe === "true")
5169
+ safe = ok;
5170
+ else {
5171
+ const both = B.tmp();
5172
+ B.line(`${both} = and i1 ${safe}, ${ok}`);
5173
+ safe = both;
5174
+ }
5175
+ }
4286
5176
  const fast = B.newLabel("counted.fast");
4287
5177
  const slow = B.newLabel("counted.slow");
4288
5178
  countedJoin = B.newLabel("counted.done");
4289
- B.line(`${safe} = fcmp ${countedLoop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(countedLoop.guardLimit)}`);
4290
5179
  B.condBr(safe, fast, slow);
4291
5180
  B.startBlock(slow);
4292
5181
  this.countedLoopsEnabled = false;
@@ -4367,7 +5256,7 @@ export class LlEmitter {
4367
5256
  };
4368
5257
  const lu = s.update || capturedLets.length > 0 ? B.newLabel("loop.u") : lc;
4369
5258
  freshenBindings();
4370
- const workerBudget = this.workerLoopBudget();
5259
+ const workerBudget = this.workerLoopBudget(s);
4371
5260
  B.br(lc);
4372
5261
  B.startBlock(lc);
4373
5262
  this.emitWorkerLoopCheck(workerBudget);
@@ -4475,7 +5364,7 @@ export class LlEmitter {
4475
5364
  const lb = B.newLabel("fof.b");
4476
5365
  const lu = B.newLabel("fof.u");
4477
5366
  const le = B.newLabel("fof.e");
4478
- const workerBudget = this.workerLoopBudget();
5367
+ const workerBudget = this.workerLoopBudget(s);
4479
5368
  B.br(lc);
4480
5369
  B.startBlock(lc);
4481
5370
  this.emitWorkerLoopCheck(workerBudget);
@@ -4494,7 +5383,7 @@ export class LlEmitter {
4494
5383
  B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
4495
5384
  this.markMemoryPointer(lenPtr, "array:header");
4496
5385
  B.line(`${rawLen} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`);
4497
- B.line(`${len} = uitofp ${this.sizeType} ${rawLen} to double`);
5386
+ B.line(`${len} = sitofp ${this.sizeType} ${rawLen} to double`);
4498
5387
  B.line(`${inBounds} = fcmp olt double ${i}, ${len}`);
4499
5388
  cur = i;
4500
5389
  }
@@ -4611,7 +5500,12 @@ export class LlEmitter {
4611
5500
  // the actual ret. The parked value owns a synthetic slot-backed
4612
5501
  // scope entry during each copy so a throwing finally releases it.
4613
5502
  let v = null;
4614
- if (s.value !== null) {
5503
+ if (s.value !== null && this.currentBorrowedReturn) {
5504
+ // A borrowed-return body hands back its walk without a reference
5505
+ // (it has no try statements, so no finally can intervene).
5506
+ v = this.emitReadReceiver(s.value);
5507
+ }
5508
+ else if (s.value !== null) {
4615
5509
  v = this.emitExpr(s.value);
4616
5510
  this.moveTemp(v);
4617
5511
  }
@@ -4986,24 +5880,62 @@ export class LlEmitter {
4986
5880
  return emitLiteralExpr(this, e);
4987
5881
  }
4988
5882
  emitOperatorExpr(e) {
4989
- if (e.kind === "seqExpr") {
4990
- // Saved operands can feed later call arguments. Transfer ownership
4991
- // to this expression's frame so they survive those reads and still
4992
- // die on the path that created them, including inside lazy branches.
4993
- this.scopes.push([]);
4994
- const result = emitBorrowedFieldSequence(this, e) ?? emitOperatorExpr(this, e);
4995
- for (const local of this.scopes.pop()) {
4996
- this.currentFrame().push({
4997
- name: local.slot,
4998
- type: local.type,
4999
- slot: true,
5000
- ...(local.boxed ? { boxed: true } : {}),
5001
- });
5002
- }
5003
- return result;
5004
- }
5883
+ if (e.kind === "seqExpr")
5884
+ return this.emitSequence(() => emitBorrowedFieldSequence(this, e) ?? emitOperatorExpr(this, e));
5005
5885
  return emitOperatorExpr(this, e);
5006
5886
  }
5887
+ /** Saved operands can feed later call arguments. Transfer ownership of a
5888
+ * sequence's locals to this expression's frame so they survive those
5889
+ * reads and still die on the path that created them, including inside
5890
+ * lazy branches. */
5891
+ emitSequence(body) {
5892
+ this.scopes.push([]);
5893
+ const result = body();
5894
+ for (const local of this.scopes.pop()) {
5895
+ this.currentFrame().push({
5896
+ name: local.slot,
5897
+ type: local.type,
5898
+ slot: true,
5899
+ ...(local.boxed ? { boxed: true } : {}),
5900
+ });
5901
+ }
5902
+ return result;
5903
+ }
5904
+ /** A statement-position expression whose value is dropped. When a
5905
+ * sequence's result only reads an unchanged local (a derived
5906
+ * constructor's `super()` evaluates to `this`), its statements still run
5907
+ * in place, but the result is neither retained nor released. */
5908
+ /** Whether a discarded value can be skipped entirely: a borrowable read
5909
+ * with no effect of its own. canBorrowCallArgument also accepts checked
5910
+ * projections (a ternary or narrow whose failing arm throws, and the
5911
+ * throwing call itself) because a borrowed use still evaluates them;
5912
+ * skipping those would drop the throw. */
5913
+ discardableRead(value) {
5914
+ if (value.kind === "libCall" || value.kind === "ternary" || value.kind === "unionNarrow")
5915
+ return false;
5916
+ if ((value.kind === "upcast" || value.kind === "downcast" || value.kind === "unionWrap") &&
5917
+ !this.discardableRead(value.value))
5918
+ return false;
5919
+ if (value.kind === "seqExpr") {
5920
+ const guarded = guardedValue(value);
5921
+ if (guarded === null || !this.discardableRead(guarded))
5922
+ return false;
5923
+ }
5924
+ return this.canBorrowCallArgument(value);
5925
+ }
5926
+ emitDiscarded(e) {
5927
+ if (e.kind === "seqExpr" &&
5928
+ isRefCounted(e.result.type) &&
5929
+ e.result.kind !== "strLit" &&
5930
+ this.discardableRead(e.result)) {
5931
+ this.emitSequence(() => {
5932
+ for (const s of e.stmts)
5933
+ this.emitStmt(s);
5934
+ });
5935
+ return;
5936
+ }
5937
+ this.emitExpr(e);
5938
+ }
5007
5939
  emitControlExpr(e) {
5008
5940
  return emitControlExpr(this, e);
5009
5941
  }
@@ -5017,8 +5949,17 @@ export class LlEmitter {
5017
5949
  * value, making the snapshot unique unless a real observable alias exists.
5018
5950
  */
5019
5951
  emitStringSelfConcatAssign(localId, left, suffix, retainForYield) {
5952
+ const moved = this.emitMovedSelfConcat(localId, left, suffix, retainForYield);
5953
+ if (moved)
5954
+ return moved;
5020
5955
  const snapshot = this.emitExpr(left);
5021
- const right = this.emitExpr(suffix);
5956
+ // A suffix of several parts, or a number, appends each operand to the
5957
+ // snapshot directly (in place once the binding lets go of it) instead
5958
+ // of first building the suffix as a separate string.
5959
+ const parts = stringParts(suffix);
5960
+ const mixed = parts.length > 1 || numberPart(parts[0]) !== null;
5961
+ const inputs = mixed ? emitConcatInputs(this, parts) : [];
5962
+ const right = mixed ? null : this.emitExpr(suffix);
5022
5963
  const b = this.binding(localId);
5023
5964
  const B = this.B;
5024
5965
  if (b.kind === "boxed") {
@@ -5031,9 +5972,15 @@ export class LlEmitter {
5031
5972
  B.line(`store ptr null, ptr ${b.slot}`);
5032
5973
  this.releaseValue(old, b.type);
5033
5974
  }
5034
- this.declare(`declare ptr @scr_str_concat(ptr, ptr)`);
5035
- const raw = B.tmp();
5036
- B.line(`${raw} = call ptr @scr_str_concat(ptr ${snapshot.name}, ptr ${right.name})`);
5975
+ let raw;
5976
+ if (right === null) {
5977
+ raw = emitMixedConcat(this, snapshot.name, inputs);
5978
+ }
5979
+ else {
5980
+ this.declare(`declare ptr @scr_str_concat(ptr, ptr)`);
5981
+ raw = B.tmp();
5982
+ B.line(`${raw} = call ptr @scr_str_concat(ptr ${snapshot.name}, ptr ${right.name})`);
5983
+ }
5037
5984
  const result = this.own({ name: raw, type: left.type });
5038
5985
  if (retainForYield) {
5039
5986
  const stored = this.retainValue(result.name, result.type);
@@ -5051,6 +5998,43 @@ export class LlEmitter {
5051
5998
  }
5052
5999
  return result;
5053
6000
  }
6001
+ /** `local = local + suffix` when the suffix cannot read or write the
6002
+ * local (an unboxed binding is only reachable through this function's own
6003
+ * expressions): reading the binding after the suffix gives the value the
6004
+ * left side had, so it moves into a consuming concat, and the result takes
6005
+ * its place. Null when the general snapshot path is needed. */
6006
+ emitMovedSelfConcat(localId, left, suffix, retainForYield) {
6007
+ const b = this.binding(localId);
6008
+ if (retainForYield ||
6009
+ b.kind !== "local" ||
6010
+ b.local === undefined ||
6011
+ b.local.boxed ||
6012
+ b.local.tdz ||
6013
+ b.type.kind !== "string" ||
6014
+ left.kind !== "varRef" ||
6015
+ left.localId !== localId ||
6016
+ mentionsLocal(suffix, localId))
6017
+ return null;
6018
+ const parts = stringParts(suffix);
6019
+ const mixed = parts.length > 1 || numberPart(parts[0]) !== null;
6020
+ const inputs = mixed ? emitConcatInputs(this, parts) : [];
6021
+ const right = mixed ? null : this.emitExpr(suffix);
6022
+ this.materializeSplitLocal(localId);
6023
+ const B = this.B;
6024
+ const head = B.tmp();
6025
+ B.line(`${head} = load ptr, ptr ${b.slot}`);
6026
+ let raw;
6027
+ if (right === null) {
6028
+ raw = emitMixedConcat(this, head, inputs, true);
6029
+ }
6030
+ else {
6031
+ this.declare(`declare ptr @scr_str_concat_move(ptr, ptr)`);
6032
+ raw = B.tmp();
6033
+ B.line(`${raw} = call ptr @scr_str_concat_move(ptr ${head}, ptr ${right.name})`);
6034
+ }
6035
+ B.line(`store ptr ${raw}, ptr ${b.slot}`);
6036
+ return { name: raw, type: left.type }; // moved into the binding
6037
+ }
5054
6038
  emitContainerExpr(e) {
5055
6039
  return emitContainerExpr(this, e);
5056
6040
  }
@@ -5064,8 +6048,152 @@ export class LlEmitter {
5064
6048
  return emitDynamicExpr(this, e);
5065
6049
  }
5066
6050
  emitIntrinsicExpr(e) {
6051
+ if (e.name === "threads.publish")
6052
+ return this.emitPublish(e);
5067
6053
  return emitIntrinsicExpr(this, e);
5068
6054
  }
6055
+ publishWalkersCache = null;
6056
+ /** Per-type publication walkers, created on the first publish. */
6057
+ get publishWalkers() {
6058
+ if (this.publishWalkersCache !== null)
6059
+ return this.publishWalkersCache;
6060
+ const byPre = [...this.classMeta.values()].sort((a, b) => a.pre - b.pre);
6061
+ this.publishWalkersCache = new PublishWalkers({
6062
+ declare: (decl) => this.declare(decl),
6063
+ cstr: (text) => this.cstr(text),
6064
+ sizeType: this.sizeType,
6065
+ tracedShapes: this.tracedShapes,
6066
+ unionsById: this.unionsById,
6067
+ recordsById: this.recordsById,
6068
+ nullableArm: (t) => this.nullableUnions.of(t)?.arm ?? null,
6069
+ classInfo: (className) => {
6070
+ const meta = this.classMetaOf(className);
6071
+ return {
6072
+ def: meta.def,
6073
+ hierarchy: meta.hierarchy,
6074
+ pre: meta.pre,
6075
+ post: meta.post,
6076
+ runtimeRooted: meta.root.def.runtime === true,
6077
+ fields: meta.def.fields.map((f) => ({
6078
+ name: f.name,
6079
+ index: classFieldIndex(meta, f.name).index,
6080
+ type: this.nullableFields.storageType(className, f.name, f.type),
6081
+ })),
6082
+ };
6083
+ },
6084
+ classesInInterval: (pre, post) => byPre.filter((m) => m.pre >= pre && m.pre <= post).map((m) => m.def.name),
6085
+ });
6086
+ return this.publishWalkersCache;
6087
+ }
6088
+ /** `publish(value)` (@scriptc/threads): makes the graph immortal and
6089
+ * immutable (scr_publish with the static type's walker) and answers the
6090
+ * same value. Throws (pending) when a dyn part is unpublishable. */
6091
+ emitPublish(e) {
6092
+ const value = this.emitExpr(e.args[0]);
6093
+ if (this.B.isTerminated())
6094
+ return value;
6095
+ const lines = this.publishWalkers.emitPublish(value.name, value.type, e.loc);
6096
+ if (lines.length > 0) {
6097
+ for (const line of lines)
6098
+ this.B.line(line.trimStart());
6099
+ // A refusal unwinds while the operand temp still owns its reference.
6100
+ this.emitPendingCheck();
6101
+ }
6102
+ // The result IS the operand: ownership moves to the result temp.
6103
+ if (!isRefCounted(value.type))
6104
+ return { name: value.name, type: e.type };
6105
+ this.moveTemp(value);
6106
+ return this.own({ name: value.name, type: e.type });
6107
+ }
6108
+ /** Branches on "`obj` is published" (rc == SIZE_MAX); leaves the builder
6109
+ * in the cold block and answers the continuation label. */
6110
+ emitPublishedTest(obj) {
6111
+ const B = this.B;
6112
+ const rc = B.tmp(), frozen = B.tmp(), expected = B.tmp();
6113
+ B.line(`${rc} = load ${this.sizeType}, ptr ${obj}`);
6114
+ B.line(`${frozen} = icmp eq ${this.sizeType} ${rc}, -1`);
6115
+ this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
6116
+ B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${frozen}, i1 false)`);
6117
+ const cold = B.newLabel("pub.frozen"), ok = B.newLabel("pub.ok");
6118
+ B.condBr(expected, cold, ok);
6119
+ B.startBlock(cold);
6120
+ return ok;
6121
+ }
6122
+ publishedNamesSym = null;
6123
+ /** The class-name table the hierarchy field guards index by preorder. */
6124
+ publishedClassNamesDefs() {
6125
+ if (this.publishedNamesSym === null)
6126
+ return [];
6127
+ const metas = [...this.classMeta.values()];
6128
+ const size = metas.reduce((max, m) => Math.max(max, m.pre + 1), 0);
6129
+ const names = new Array(size).fill("ptr null");
6130
+ for (const m of metas)
6131
+ names[m.pre] = `ptr ${this.cstr(m.def.jsName ?? m.def.name)}`;
6132
+ return [
6133
+ `${this.publishedNamesSym} = internal constant [${size} x ptr] [ ${names.join(", ")} ]`,
6134
+ ``,
6135
+ ];
6136
+ }
6137
+ /** The frozen-object guard of a class field or record field store:
6138
+ * Node's "Cannot assign to read only property" TypeError when the target
6139
+ * is published. `className` null means a record (`#<Object>`). */
6140
+ emitPublishedFieldGuard(obj, className, field) {
6141
+ const published = this.publishedTypes;
6142
+ if (published === null)
6143
+ return;
6144
+ if (className !== null && !published.classGuarded(className))
6145
+ return;
6146
+ const B = this.B;
6147
+ const ok = this.emitPublishedTest(obj);
6148
+ let owner;
6149
+ const meta = className === null ? undefined : this.classMeta.get(className);
6150
+ if (meta === undefined)
6151
+ owner = this.cstr("Object");
6152
+ else if (!meta.hierarchy)
6153
+ owner = this.cstr(meta.def.jsName ?? meta.def.name);
6154
+ else {
6155
+ // The DYNAMIC class names the object, as in Node.
6156
+ this.publishedNamesSym ??= "@sc_pub_class_names";
6157
+ const vtp = B.tmp(), vt = B.tmp(), pre = B.tmp(), slot = B.tmp(), name = B.tmp();
6158
+ B.line(`${vtp} = getelementptr inbounds ptr, ptr ${obj}, i64 1`);
6159
+ B.line(`${vt} = load ptr, ptr ${vtp}`);
6160
+ B.line(`${pre} = load ${this.sizeType}, ptr ${vt}`);
6161
+ B.line(`${slot} = getelementptr inbounds ptr, ptr ${this.publishedNamesSym}, ${this.sizeType} ${pre}`);
6162
+ B.line(`${name} = load ptr, ptr ${slot}`);
6163
+ owner = name;
6164
+ }
6165
+ this.declare(`declare void @scr_throw_published_field(ptr, ptr)`);
6166
+ B.line(`call void @scr_throw_published_field(ptr ${this.cstr(field)}, ptr ${owner})`);
6167
+ this.emitUnwind();
6168
+ B.startBlock(ok);
6169
+ }
6170
+ /** The frozen-array guard (scr_throw_published_array's ops): `index` and
6171
+ * `count` are doubles (the written index, the argument count). */
6172
+ emitPublishedArrayGuard(arr, arrType, op, index = f64Lit(0), count = f64Lit(0)) {
6173
+ if (this.publishedTypes?.containerGuarded(arrType) !== true)
6174
+ return;
6175
+ const B = this.B;
6176
+ const ok = this.emitPublishedTest(arr);
6177
+ this.declare(`declare zeroext i1 @scr_throw_published_array(i32, ptr, double, double)`);
6178
+ const threw = B.tmp();
6179
+ B.line(`${threw} = call zeroext i1 @scr_throw_published_array(i32 ${op}, ptr ${arr}, double ${index}, double ${count})`);
6180
+ const unwind = B.newLabel("pub.throw");
6181
+ B.condBr(threw, unwind, ok);
6182
+ B.startBlock(unwind);
6183
+ this.emitUnwind();
6184
+ B.startBlock(ok);
6185
+ }
6186
+ /** The published Map/Set guard: "Cannot modify a published Map/Set". */
6187
+ emitPublishedCollectionGuard(m, t) {
6188
+ if (this.publishedTypes?.containerGuarded(t) !== true)
6189
+ return;
6190
+ const B = this.B;
6191
+ const ok = this.emitPublishedTest(m);
6192
+ this.declare(`declare void @scr_throw_published_collection(i1 zeroext)`);
6193
+ B.line(`call void @scr_throw_published_collection(i1 ${t.kind === "set" ? "true" : "false"})`);
6194
+ this.emitUnwind();
6195
+ B.startBlock(ok);
6196
+ }
5069
6197
  emitSerializationExpr(e) {
5070
6198
  return emitSerializationExpr(this, e);
5071
6199
  }
@@ -5085,6 +6213,13 @@ export class LlEmitter {
5085
6213
  return this.emitExpr(e);
5086
6214
  if (e.kind === "strLit")
5087
6215
  return { name: this.internLiteral(e.value), type: e.type };
6216
+ const guarded = guardedValue(e);
6217
+ if (guarded !== null) {
6218
+ // Throwing guards run in place, exactly as the sequence emits them.
6219
+ for (const s of e.stmts)
6220
+ this.emitStmt(s);
6221
+ return this.emitReadReceiver(guarded);
6222
+ }
5088
6223
  if (e.kind === "varRef") {
5089
6224
  this.materializeSplitLocal(e.localId);
5090
6225
  const binding = this.binding(e.localId);
@@ -5106,7 +6241,32 @@ export class LlEmitter {
5106
6241
  this.B.line(`${value} = load ptr, ptr ${binding.slot}`);
5107
6242
  return { name: value, type: e.type };
5108
6243
  }
5109
- const read = matchMapRead(e, this.unionsById);
6244
+ if (e.kind === "chainRecv") {
6245
+ // The chain's bind slot holds its receiver unchanged until the body
6246
+ // finishes, owned by the slot itself or by the chain's receiver.
6247
+ const bound = this.chainSlots.get(e.id);
6248
+ if (!bound)
6249
+ throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`);
6250
+ const value = this.B.tmp();
6251
+ this.B.line(`${value} = load ${this.llType(bound.type)}, ptr ${bound.name}`);
6252
+ return { name: value, type: e.type };
6253
+ }
6254
+ if (e.kind === "arrayGet" && this.canBorrowReceiver(e)) {
6255
+ // The array keeps the element alive until the consumer; a missing
6256
+ // element still traps exactly like the owned read.
6257
+ const slot = this.B.slot();
6258
+ this.B.entryAllocas.push(`${slot} = alloca ptr`);
6259
+ emitBorrowedArrayRead(this, e, slot);
6260
+ const value = this.B.tmp();
6261
+ this.B.line(`${value} = load ptr, ptr ${slot}`);
6262
+ return { name: value, type: e.type };
6263
+ }
6264
+ const wrap = this.borrowableNullableWrap(e);
6265
+ if (wrap === "unit" && e.kind === "unionWrap")
6266
+ return { name: this.unitInstanceRef(e.unionId, e.tag), type: e.type };
6267
+ if (wrap === "ref" && e.kind === "unionWrap")
6268
+ return { name: this.emitReadReceiver(e.value).name, type: e.type };
6269
+ const read = matchMapRead(e, this.boxedUnionsById);
5110
6270
  if (read) {
5111
6271
  const result = emitStackMapRead(this, read);
5112
6272
  if (result.owner)
@@ -5116,11 +6276,19 @@ export class LlEmitter {
5116
6276
  const arrayRead = this.optionalArrayReads.get(e);
5117
6277
  if (arrayRead)
5118
6278
  return emitProjectedArrayRead(this, arrayRead);
5119
- if (canStackUnion(e, this.unionsById))
6279
+ const narrow = this.checkedNarrows.get(e);
6280
+ const narrowed = narrow ? emitCheckedNarrowReceiver(this, narrow) : null;
6281
+ if (narrowed)
6282
+ return narrowed;
6283
+ if (canStackUnion(e, this.boxedUnionsById))
5120
6284
  return emitStackUnion(this, e).value;
5121
6285
  if (e.kind === "unionNarrow") {
5122
6286
  const union = this.emitUnionProjection(e.value);
5123
- return { name: this.unionPeek(union.name), type: e.type };
6287
+ return { name: this.unionPeek(union.name, e.unionId), type: e.type };
6288
+ }
6289
+ if ((e.kind === "downcast" || e.kind === "upcast") && e.value.type.kind === "object") {
6290
+ // Prefix layouts: a class cast reads the operand's own pointer.
6291
+ return { name: this.emitReadReceiver(e.value).name, type: e.type };
5124
6292
  }
5125
6293
  // A nullable-pointer field has no box to borrow. Projections use
5126
6294
  // emitUnionProjection; other borrowers get an owned heap box.
@@ -5133,6 +6301,8 @@ export class LlEmitter {
5133
6301
  return { name: this.loadRecordField(ptr, type), type: e.type };
5134
6302
  }
5135
6303
  const { ptr, type } = this.classFieldPtr(receiver.name, e.className, e.field);
6304
+ if (this.int32Slots.isField(e.className, e.field))
6305
+ return this.loadInt32Field(ptr, e.type);
5136
6306
  return { name: this.loadField(ptr, type), type: e.type };
5137
6307
  }
5138
6308
  if (e.kind === "ternary" && this.canBorrowReceiver(e.then) && this.canBorrowReceiver(e.else_)) {
@@ -5160,6 +6330,8 @@ export class LlEmitter {
5160
6330
  B.line(`${value} = load ptr, ptr ${slot}`);
5161
6331
  return { name: value, type: e.type };
5162
6332
  }
6333
+ if (e.kind === "call" && this.canBorrowReceiver(e))
6334
+ return emitBorrowedResultCall(this, e);
5163
6335
  return this.emitExpr(e);
5164
6336
  }
5165
6337
  /** A private stack union source with an independently owned payload. */
@@ -5171,8 +6343,8 @@ export class LlEmitter {
5171
6343
  isStackUnionSource(e) {
5172
6344
  if (e.type.kind !== "union")
5173
6345
  return false;
5174
- return (canStackUnion(e, this.unionsById) ||
5175
- matchMapRead(e, this.unionsById) !== null ||
6346
+ return (canStackUnion(e, this.boxedUnionsById) ||
6347
+ matchMapRead(e, this.boxedUnionsById) !== null ||
5176
6348
  this.optionalArrayReads.get(e) !== null);
5177
6349
  }
5178
6350
  /** The nullable-pointer representation behind a union-typed class field
@@ -5244,7 +6416,10 @@ export class LlEmitter {
5244
6416
  B.br(join);
5245
6417
  B.startBlock(ref);
5246
6418
  const owned = this.retainValue(p, nullable.arm);
5247
- const box = this.unionNewOwned(nullable.refTag, { name: owned, type: nullable.arm });
6419
+ const box = this.unionNewOwned(nullable.unionId, nullable.refTag, {
6420
+ name: owned,
6421
+ type: nullable.arm,
6422
+ });
5248
6423
  B.line(`store ptr ${box}, ptr ${slot}`);
5249
6424
  B.br(join);
5250
6425
  B.startBlock(join);
@@ -5256,10 +6431,10 @@ export class LlEmitter {
5256
6431
  * box, which stays owned by its existing owner. */
5257
6432
  unionToNullable(u, nullable) {
5258
6433
  const B = this.B;
5259
- const tag = this.unionTag(u);
6434
+ const tag = this.unionTag(u, nullable.unionId);
5260
6435
  const isRef = B.tmp(), sel = B.tmp();
5261
6436
  B.line(`${isRef} = icmp eq i32 ${tag}, ${nullable.refTag}`);
5262
- const payload = this.unionPeek(u);
6437
+ const payload = this.unionPeek(u, nullable.unionId);
5263
6438
  B.line(`${sel} = select i1 ${isRef}, ptr ${payload}, ptr null`);
5264
6439
  return this.retainValue(sel, nullable.arm);
5265
6440
  }
@@ -5311,6 +6486,8 @@ export class LlEmitter {
5311
6486
  }
5312
6487
  canBorrowReceiver(e) {
5313
6488
  switch (e.kind) {
6489
+ case "chainRecv":
6490
+ return this.chainSlots.has(e.id);
5314
6491
  case "varRef": {
5315
6492
  const binding = this.binding(e.localId);
5316
6493
  return (binding.kind !== "boxed" ||
@@ -5320,20 +6497,166 @@ export class LlEmitter {
5320
6497
  isRefCounted(e.type)));
5321
6498
  }
5322
6499
  case "unionNarrow":
6500
+ // Class casts reinterpret the same pointer (prefix layouts).
6501
+ case "downcast":
6502
+ case "upcast":
5323
6503
  return this.canBorrowReceiver(e.value);
5324
6504
  case "fieldGet":
5325
6505
  case "recordGet":
5326
6506
  return this.canBorrowReceiver(e.obj);
5327
6507
  case "ternary":
5328
6508
  return this.canBorrowReceiver(e.then) && this.canBorrowReceiver(e.else_);
6509
+ case "seqExpr": {
6510
+ const guarded = guardedValue(e);
6511
+ return guarded !== null && this.canBorrowReceiver(guarded);
6512
+ }
5329
6513
  // The runtime always sets a pending exception; its unreachable
5330
6514
  // typed dummy is null and owns no receiver.
5331
6515
  case "libCall":
5332
6516
  return e.fn === "error.nodeThrow";
6517
+ case "unionWrap": {
6518
+ const wrap = this.borrowableNullableWrap(e);
6519
+ return wrap === "unit" || (wrap === "ref" && this.canBorrowReceiver(e.value));
6520
+ }
6521
+ // A borrowed-return callee preserves edges, so its result stays
6522
+ // reachable from borrowable arguments until the consumer runs.
6523
+ case "call":
6524
+ return (this.borrowedReturns.has(e.callee) &&
6525
+ e.args.every((arg) => (!isRefCounted(arg.type) || this.canBorrowReceiver(arg)) &&
6526
+ this.referenceEffects.preserves(arg)));
6527
+ // A required element read: the (borrowed) array owns the element
6528
+ // while an edge-preserving index computes.
6529
+ case "arrayGet":
6530
+ return (e.arr.type.kind === "array" &&
6531
+ isRefCounted(e.arr.type.elem) &&
6532
+ this.canBorrowReceiver(e.arr) &&
6533
+ this.referenceEffects.preserves(e.index));
5333
6534
  default:
5334
6535
  return false;
5335
6536
  }
5336
6537
  }
6538
+ /** A reference held as one plain pointer that emitReadReceiver can read
6539
+ * without owning: class instances, records, arrays, strings and
6540
+ * nullable-pointer unions. Tagged union boxes, closures and dynamic
6541
+ * values keep ordinary ownership. */
6542
+ plainReference(type) {
6543
+ switch (type.kind) {
6544
+ case "object":
6545
+ case "record":
6546
+ case "array":
6547
+ case "string":
6548
+ return true;
6549
+ case "union":
6550
+ return this.nullableUnions.has(type.unionId);
6551
+ default:
6552
+ return false;
6553
+ }
6554
+ }
6555
+ /** Functions returning a walk of their borrowed parameters return it
6556
+ * without a reference (+0) from their borrowing body; direct callers
6557
+ * either consume it as a receiver/walk or retain it at once, and the owned
6558
+ * adapter retains before releasing its parameters. Candidates preserve
6559
+ * every heap edge (so the result stays reachable from the arguments),
6560
+ * borrow every reference parameter and return plain pointers. The set
6561
+ * grows to a fixpoint because a call to a borrowed-return function with
6562
+ * walk arguments is itself a walk; walk facts are then recomputed with
6563
+ * the final set, admitting only parameters whose convention borrows. */
6564
+ analyzeBorrowedReturns() {
6565
+ const pointer = (type) => this.plainReference(type);
6566
+ const borrowedIds = (fn) => {
6567
+ const indexes = this.callLifetimes.borrowed.get(fn.name);
6568
+ return new Set(fn.params.filter((_, i) => indexes?.has(i)).map((p) => p.localId));
6569
+ };
6570
+ const hostFor = (fn) => {
6571
+ const allowed = borrowedIds(fn);
6572
+ return {
6573
+ pointerLocal: (local) => pointer(local.type),
6574
+ borrowsWithoutOwning: (e) => this.borrowsWithoutOwning(e),
6575
+ borrowedReturn: (callee) => this.borrowedReturns.has(callee),
6576
+ parameterAllowed: (id) => allowed.has(id),
6577
+ };
6578
+ };
6579
+ const preserving = [...this.fnByName.values()].filter((fn) => this.referenceEffects.functions.has(fn.name));
6580
+ const candidates = preserving.filter((fn) => {
6581
+ if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures)
6582
+ return false;
6583
+ // A borrowing vtable slot dispatches to the borrowing body (or its
6584
+ // virtual adapter) directly, and virtual callers own the result.
6585
+ if (this.implSlotBorrowed(fn.name).size > 0)
6586
+ return false;
6587
+ const indexes = this.callLifetimes.borrowed.get(fn.name);
6588
+ return (indexes !== undefined &&
6589
+ indexes.size > 0 &&
6590
+ fn.params.every((p, i) => !isRefCounted(p.type) || indexes.has(i)));
6591
+ });
6592
+ for (let changed = true; changed;) {
6593
+ changed = false;
6594
+ for (const fn of candidates) {
6595
+ if (this.borrowedReturns.has(fn.name))
6596
+ continue;
6597
+ if (!analyzeWalks(fn, hostFor(fn)).returnsWalk)
6598
+ continue;
6599
+ this.borrowedReturns.add(fn.name);
6600
+ changed = true;
6601
+ }
6602
+ }
6603
+ if (this.borrowedReturns.size === 0)
6604
+ return;
6605
+ for (const fn of preserving) {
6606
+ const walks = analyzeWalks(fn, hostFor(fn)).locals;
6607
+ if (walks.size > 0)
6608
+ this.walkBorrowsByFunction.set(fn.name, walks);
6609
+ }
6610
+ }
6611
+ /** emitReadReceiver produces this node's pointer without acquiring a
6612
+ * reference, given operands (and ternary arms) that do the same: plain
6613
+ * class/record field reads, class casts, nullable-pointer narrows and
6614
+ * wraps, and checked ternaries. Stack-boxed, map-read and boxed-field
6615
+ * sources are owned. Depends only on module facts, not the current
6616
+ * function. */
6617
+ borrowsWithoutOwning(e) {
6618
+ if (!isRefCounted(e.type) || this.isStackUnionSource(e))
6619
+ return false;
6620
+ if (matchMapRead(e, this.boxedUnionsById))
6621
+ return false;
6622
+ switch (e.kind) {
6623
+ case "fieldGet":
6624
+ return this.nullableFieldGet(e) === null;
6625
+ case "recordGet":
6626
+ return true;
6627
+ case "unionNarrow":
6628
+ return this.nullableUnions.has(e.unionId);
6629
+ case "downcast":
6630
+ case "upcast":
6631
+ return e.value.type.kind === "object";
6632
+ case "unionWrap":
6633
+ return this.borrowableNullableWrap(e) !== null;
6634
+ // The caller proves each arm separately.
6635
+ case "ternary":
6636
+ return true;
6637
+ // The caller proves the arguments are walks; their evaluation must not
6638
+ // remove an edge before the call reads them.
6639
+ case "call":
6640
+ return (this.borrowedReturns.has(e.callee) &&
6641
+ e.args.every((arg) => this.referenceEffects.preserves(arg)));
6642
+ default:
6643
+ return false;
6644
+ }
6645
+ }
6646
+ /** A wrap into a nullable-pointer union is the payload pointer itself
6647
+ * (the reference arm) or an immortal constant (a unit arm), so it can
6648
+ * borrow exactly when its payload can. Void payloads run for effects and
6649
+ * keep the ordinary path; tagged unions still construct a box. */
6650
+ borrowableNullableWrap(e) {
6651
+ if (e.kind !== "unionWrap")
6652
+ return null;
6653
+ const nullable = this.nullableUnions.get(e.unionId);
6654
+ if (!nullable)
6655
+ return null;
6656
+ if (isUnitType(e.value.type))
6657
+ return e.value.kind === "unitLit" ? "unit" : null;
6658
+ return e.tag === nullable.refTag && isRefCounted(e.value.type) ? "ref" : null;
6659
+ }
5337
6660
  materializeSplitLocal(localId) {
5338
6661
  const span = this.splitSpans.get(localId);
5339
6662
  if (span)
@@ -5420,8 +6743,8 @@ export class LlEmitter {
5420
6743
  resolveThunkFor(inner) {
5421
6744
  return resolveThunkFor(this, inner);
5422
6745
  }
5423
- tagInSet(uName, tags) {
5424
- return tagInSet(this, uName, tags);
6746
+ tagInSet(uName, unionId, tags) {
6747
+ return tagInSet(this, uName, unionId, tags);
5425
6748
  }
5426
6749
  arrPush(arr, acc, value) {
5427
6750
  return arrPush(this, arr, acc, value);
@@ -5465,8 +6788,8 @@ export class LlEmitter {
5465
6788
  emitBytesLength(elem, receiver, bytes) {
5466
6789
  return emitBytesLength(this, elem, receiver, bytes);
5467
6790
  }
5468
- emitBytesGet(elem, receiver, index, expr, inBounds = false) {
5469
- return emitBytesGet(this, elem, receiver, index, expr, inBounds);
6791
+ emitBytesGet(elem, receiver, index, expr, inBounds = false, invalidNaN = false) {
6792
+ return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN);
5470
6793
  }
5471
6794
  emitToUint32(value, expr, uint32) {
5472
6795
  return emitToUint32(this, value, expr, uint32);