@scriptc/compiler 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +2 -0
  2. package/dist/backend/llvm/alloc.d.ts +3 -0
  3. package/dist/backend/llvm/alloc.js +54 -13
  4. package/dist/backend/llvm/alloc.js.map +1 -1
  5. package/dist/backend/llvm/blocks.d.ts +9 -0
  6. package/dist/backend/llvm/blocks.js +9 -0
  7. package/dist/backend/llvm/blocks.js.map +1 -1
  8. package/dist/backend/llvm/borrowed-receivers.d.ts +7 -0
  9. package/dist/backend/llvm/borrowed-receivers.js +39 -0
  10. package/dist/backend/llvm/borrowed-receivers.js.map +1 -1
  11. package/dist/backend/llvm/call-lifetimes.d.ts +4 -1
  12. package/dist/backend/llvm/call-lifetimes.js +14 -3
  13. package/dist/backend/llvm/call-lifetimes.js.map +1 -1
  14. package/dist/backend/llvm/call-lifetimes.test.js +8 -3
  15. package/dist/backend/llvm/call-lifetimes.test.js.map +1 -1
  16. package/dist/backend/llvm/checked-narrows.d.ts +24 -0
  17. package/dist/backend/llvm/checked-narrows.js +108 -0
  18. package/dist/backend/llvm/checked-narrows.js.map +1 -0
  19. package/dist/backend/llvm/checked-narrows.test.d.ts +1 -0
  20. package/dist/backend/llvm/checked-narrows.test.js +208 -0
  21. package/dist/backend/llvm/checked-narrows.test.js.map +1 -0
  22. package/dist/backend/llvm/classes.d.ts +13 -1
  23. package/dist/backend/llvm/classes.js +26 -18
  24. package/dist/backend/llvm/classes.js.map +1 -1
  25. package/dist/backend/llvm/classes.test.js +32 -3
  26. package/dist/backend/llvm/classes.test.js.map +1 -1
  27. package/dist/backend/llvm/debug-info.d.ts +6 -2
  28. package/dist/backend/llvm/debug-info.js +11 -1
  29. package/dist/backend/llvm/debug-info.js.map +1 -1
  30. package/dist/backend/llvm/dense-array-access.test.js +89 -0
  31. package/dist/backend/llvm/dense-array-access.test.js.map +1 -1
  32. package/dist/backend/llvm/dyn.d.ts +4 -0
  33. package/dist/backend/llvm/dyn.js +31 -20
  34. package/dist/backend/llvm/dyn.js.map +1 -1
  35. package/dist/backend/llvm/emitter.d.ts +184 -11
  36. package/dist/backend/llvm/emitter.js +1189 -125
  37. package/dist/backend/llvm/emitter.js.map +1 -1
  38. package/dist/backend/llvm/emitter.test.js +21 -1
  39. package/dist/backend/llvm/emitter.test.js.map +1 -1
  40. package/dist/backend/llvm/expr-async.js +4 -4
  41. package/dist/backend/llvm/expr-async.js.map +1 -1
  42. package/dist/backend/llvm/expr-bytes.d.ts +1 -1
  43. package/dist/backend/llvm/expr-bytes.js +85 -8
  44. package/dist/backend/llvm/expr-bytes.js.map +1 -1
  45. package/dist/backend/llvm/expr-callbacks.js +29 -21
  46. package/dist/backend/llvm/expr-callbacks.js.map +1 -1
  47. package/dist/backend/llvm/expr-calls.d.ts +5 -0
  48. package/dist/backend/llvm/expr-calls.js +55 -10
  49. package/dist/backend/llvm/expr-calls.js.map +1 -1
  50. package/dist/backend/llvm/expr-containers.d.ts +7 -1
  51. package/dist/backend/llvm/expr-containers.js +230 -27
  52. package/dist/backend/llvm/expr-containers.js.map +1 -1
  53. package/dist/backend/llvm/expr-control.js +25 -10
  54. package/dist/backend/llvm/expr-control.js.map +1 -1
  55. package/dist/backend/llvm/expr-dynamic.js +72 -21
  56. package/dist/backend/llvm/expr-dynamic.js.map +1 -1
  57. package/dist/backend/llvm/expr-island.js +3 -8
  58. package/dist/backend/llvm/expr-island.js.map +1 -1
  59. package/dist/backend/llvm/expr-primitives.js +41 -12
  60. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  61. package/dist/backend/llvm/expr-primitives.test.js +50 -2
  62. package/dist/backend/llvm/expr-primitives.test.js.map +1 -1
  63. package/dist/backend/llvm/expr-records.js +28 -14
  64. package/dist/backend/llvm/expr-records.js.map +1 -1
  65. package/dist/backend/llvm/expr-stream-bridges.js +41 -31
  66. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  67. package/dist/backend/llvm/expr-stream-callbacks.js +6 -3
  68. package/dist/backend/llvm/expr-stream-callbacks.js.map +1 -1
  69. package/dist/backend/llvm/layouts.d.ts +2 -1
  70. package/dist/backend/llvm/layouts.js +2 -2
  71. package/dist/backend/llvm/layouts.js.map +1 -1
  72. package/dist/backend/llvm/lib-abi.js +10 -0
  73. package/dist/backend/llvm/lib-abi.js.map +1 -1
  74. package/dist/backend/llvm/lib-filesystem.js +7 -11
  75. package/dist/backend/llvm/lib-filesystem.js.map +1 -1
  76. package/dist/backend/llvm/lib-process.js +8 -8
  77. package/dist/backend/llvm/lib-process.js.map +1 -1
  78. package/dist/backend/llvm/lib-shared.js +1 -0
  79. package/dist/backend/llvm/lib-shared.js.map +1 -1
  80. package/dist/backend/llvm/local-array-reads.js +73 -13
  81. package/dist/backend/llvm/local-array-reads.js.map +1 -1
  82. package/dist/backend/llvm/local-array-reads.test.js +7 -3
  83. package/dist/backend/llvm/local-array-reads.test.js.map +1 -1
  84. package/dist/backend/llvm/local-union-storage.js +10 -6
  85. package/dist/backend/llvm/local-union-storage.js.map +1 -1
  86. package/dist/backend/llvm/local-union-storage.test.js +3 -2
  87. package/dist/backend/llvm/local-union-storage.test.js.map +1 -1
  88. package/dist/backend/llvm/map-number-lookup.d.ts +18 -0
  89. package/dist/backend/llvm/map-number-lookup.js +62 -0
  90. package/dist/backend/llvm/map-number-lookup.js.map +1 -0
  91. package/dist/backend/llvm/map-read-lifetimes.d.ts +6 -0
  92. package/dist/backend/llvm/map-read-lifetimes.js +24 -3
  93. package/dist/backend/llvm/map-read-lifetimes.js.map +1 -1
  94. package/dist/backend/llvm/map-read-lifetimes.test.js +38 -19
  95. package/dist/backend/llvm/map-read-lifetimes.test.js.map +1 -1
  96. package/dist/backend/llvm/nullable-unions.d.ts +68 -0
  97. package/dist/backend/llvm/nullable-unions.js +132 -0
  98. package/dist/backend/llvm/nullable-unions.js.map +1 -0
  99. package/dist/backend/llvm/publish.d.ts +58 -0
  100. package/dist/backend/llvm/publish.js +286 -0
  101. package/dist/backend/llvm/publish.js.map +1 -0
  102. package/dist/backend/llvm/rebound-parameters.d.ts +12 -0
  103. package/dist/backend/llvm/rebound-parameters.js +72 -0
  104. package/dist/backend/llvm/rebound-parameters.js.map +1 -0
  105. package/dist/backend/llvm/rebound-parameters.test.d.ts +1 -0
  106. package/dist/backend/llvm/rebound-parameters.test.js +67 -0
  107. package/dist/backend/llvm/rebound-parameters.test.js.map +1 -0
  108. package/dist/backend/llvm/reference-effects.d.ts +18 -2
  109. package/dist/backend/llvm/reference-effects.js +106 -16
  110. package/dist/backend/llvm/reference-effects.js.map +1 -1
  111. package/dist/backend/llvm/reference-effects.test.js +52 -1
  112. package/dist/backend/llvm/reference-effects.test.js.map +1 -1
  113. package/dist/backend/llvm/shapes.d.ts +45 -17
  114. package/dist/backend/llvm/shapes.js +131 -53
  115. package/dist/backend/llvm/shapes.js.map +1 -1
  116. package/dist/backend/llvm/shapes.test.js +33 -8
  117. package/dist/backend/llvm/shapes.test.js.map +1 -1
  118. package/dist/backend/llvm/stack-unions.test.js +5 -2
  119. package/dist/backend/llvm/stack-unions.test.js.map +1 -1
  120. package/dist/backend/llvm/string-construction.d.ts +10 -0
  121. package/dist/backend/llvm/string-construction.js +42 -0
  122. package/dist/backend/llvm/string-construction.js.map +1 -1
  123. package/dist/backend/llvm/string-key-hash.d.ts +9 -2
  124. package/dist/backend/llvm/string-key-hash.js +20 -3
  125. package/dist/backend/llvm/string-key-hash.js.map +1 -1
  126. package/dist/backend/llvm/string-key-hash.test.js +14 -3
  127. package/dist/backend/llvm/string-key-hash.test.js.map +1 -1
  128. package/dist/backend/llvm/string-lifetimes.test.js +24 -11
  129. package/dist/backend/llvm/string-lifetimes.test.js.map +1 -1
  130. package/dist/backend/llvm/string-reads.d.ts +7 -0
  131. package/dist/backend/llvm/string-reads.js +76 -0
  132. package/dist/backend/llvm/string-reads.js.map +1 -0
  133. package/dist/backend/llvm/switch-dispatch.test.js +1 -1
  134. package/dist/backend/llvm/switch-dispatch.test.js.map +1 -1
  135. package/dist/backend/llvm/test-boxed-unions.d.ts +5 -0
  136. package/dist/backend/llvm/test-boxed-unions.js +13 -0
  137. package/dist/backend/llvm/test-boxed-unions.js.map +1 -0
  138. package/dist/backend/llvm/union-repr.d.ts +15 -0
  139. package/dist/backend/llvm/union-repr.js +89 -0
  140. package/dist/backend/llvm/union-repr.js.map +1 -0
  141. package/dist/backend/llvm/virtual-borrows.d.ts +31 -0
  142. package/dist/backend/llvm/virtual-borrows.js +82 -0
  143. package/dist/backend/llvm/virtual-borrows.js.map +1 -0
  144. package/dist/backend/llvm/walk-borrows.d.ts +43 -0
  145. package/dist/backend/llvm/walk-borrows.js +150 -0
  146. package/dist/backend/llvm/walk-borrows.js.map +1 -0
  147. package/dist/backend/llvm/walk-borrows.test.d.ts +1 -0
  148. package/dist/backend/llvm/walk-borrows.test.js +119 -0
  149. package/dist/backend/llvm/walk-borrows.test.js.map +1 -0
  150. package/dist/backend/llvm/walkers.d.ts +5 -1
  151. package/dist/backend/llvm/walkers.js +24 -21
  152. package/dist/backend/llvm/walkers.js.map +1 -1
  153. package/dist/backend/may-throw.d.ts +29 -1
  154. package/dist/backend/may-throw.js +346 -3
  155. package/dist/backend/may-throw.js.map +1 -1
  156. package/dist/backend/may-throw.test.js +113 -1
  157. package/dist/backend/may-throw.test.js.map +1 -1
  158. package/dist/backend/native/runtime-inputs.d.ts +1 -1
  159. package/dist/backend/native/runtime-inputs.js +1 -0
  160. package/dist/backend/native/runtime-inputs.js.map +1 -1
  161. package/dist/backend/native-codegen-core.d.ts +7 -5
  162. package/dist/backend/native-codegen-core.js +14 -9
  163. package/dist/backend/native-codegen-core.js.map +1 -1
  164. package/dist/backend/native-codegen.d.ts +4 -0
  165. package/dist/backend/native-codegen.js +49 -2
  166. package/dist/backend/native-codegen.js.map +1 -1
  167. package/dist/backend/native-codegen.test.js +41 -10
  168. package/dist/backend/native-codegen.test.js.map +1 -1
  169. package/dist/backend/native-tools.d.ts +2 -0
  170. package/dist/backend/native-tools.js +3 -0
  171. package/dist/backend/native-tools.js.map +1 -1
  172. package/dist/backend/publication.d.ts +28 -0
  173. package/dist/backend/publication.js +155 -0
  174. package/dist/backend/publication.js.map +1 -0
  175. package/dist/backend/publication.test.d.ts +1 -0
  176. package/dist/backend/publication.test.js +72 -0
  177. package/dist/backend/publication.test.js.map +1 -0
  178. package/dist/backend/runtime-abi.d.ts +2 -2
  179. package/dist/backend/runtime-abi.js +2 -2
  180. package/dist/backend/runtime-pack-targets.test.js +1 -1
  181. package/dist/backend/runtime-pack.test.js +1 -1
  182. package/dist/cli/command.js +70 -7
  183. package/dist/cli/command.js.map +1 -1
  184. package/dist/cli/usage.d.ts +4 -1
  185. package/dist/cli/usage.js +8 -1
  186. package/dist/cli/usage.js.map +1 -1
  187. package/dist/compile-types.d.ts +12 -3
  188. package/dist/coverage/report.d.ts +25 -2
  189. package/dist/coverage/report.js +161 -33
  190. package/dist/coverage/report.js.map +1 -1
  191. package/dist/coverage/verdict-cache.d.ts +63 -0
  192. package/dist/coverage/verdict-cache.js +193 -0
  193. package/dist/coverage/verdict-cache.js.map +1 -0
  194. package/dist/coverage/verdict-cache.test.d.ts +1 -0
  195. package/dist/coverage/verdict-cache.test.js +52 -0
  196. package/dist/coverage/verdict-cache.test.js.map +1 -0
  197. package/dist/coverage/verdict-semantic.d.ts +2 -0
  198. package/dist/coverage/verdict-semantic.js +24 -0
  199. package/dist/coverage/verdict-semantic.js.map +1 -0
  200. package/dist/coverage/verdict-startup.d.ts +15 -0
  201. package/dist/coverage/verdict-startup.js +66 -0
  202. package/dist/coverage/verdict-startup.js.map +1 -0
  203. package/dist/diagnostics/diagnostic.d.ts +29 -1
  204. package/dist/diagnostics/diagnostic.js +74 -4
  205. package/dist/diagnostics/diagnostic.js.map +1 -1
  206. package/dist/diagnostics/envelope.d.ts +130 -0
  207. package/dist/diagnostics/envelope.js +257 -0
  208. package/dist/diagnostics/envelope.js.map +1 -0
  209. package/dist/diagnostics/envelope.test.d.ts +1 -0
  210. package/dist/diagnostics/envelope.test.js +91 -0
  211. package/dist/diagnostics/envelope.test.js.map +1 -0
  212. package/dist/diagnostics/render.d.ts +3 -0
  213. package/dist/diagnostics/render.js +13 -2
  214. package/dist/diagnostics/render.js.map +1 -1
  215. package/dist/executable/prepare.d.ts +4 -0
  216. package/dist/executable/prepare.js +7 -1
  217. package/dist/executable/prepare.js.map +1 -1
  218. package/dist/frontend/analysis.js +1 -0
  219. package/dist/frontend/analysis.js.map +1 -1
  220. package/dist/frontend/lowering/array-values.d.ts +7 -0
  221. package/dist/frontend/lowering/array-values.js +120 -4
  222. package/dist/frontend/lowering/array-values.js.map +1 -1
  223. package/dist/frontend/lowering/builtins/numbers.js +29 -1
  224. package/dist/frontend/lowering/builtins/numbers.js.map +1 -1
  225. package/dist/frontend/lowering/builtins/process.d.ts +4 -0
  226. package/dist/frontend/lowering/builtins/process.js +10 -0
  227. package/dist/frontend/lowering/builtins/process.js.map +1 -1
  228. package/dist/frontend/lowering/builtins/workers.js +4 -2
  229. package/dist/frontend/lowering/builtins/workers.js.map +1 -1
  230. package/dist/frontend/lowering/class-assertions.d.ts +8 -0
  231. package/dist/frontend/lowering/class-assertions.js +217 -1
  232. package/dist/frontend/lowering/class-assertions.js.map +1 -1
  233. package/dist/frontend/lowering/class-dynamic-dispatch.js +8 -5
  234. package/dist/frontend/lowering/class-dynamic-dispatch.js.map +1 -1
  235. package/dist/frontend/lowering/coercions/state.d.ts +6 -0
  236. package/dist/frontend/lowering/coercions/state.js +2 -0
  237. package/dist/frontend/lowering/coercions/state.js.map +1 -1
  238. package/dist/frontend/lowering/coercions/unions.d.ts +8 -0
  239. package/dist/frontend/lowering/coercions/unions.js +67 -2
  240. package/dist/frontend/lowering/coercions/unions.js.map +1 -1
  241. package/dist/frontend/lowering/collation-repertoire.d.ts +1 -0
  242. package/dist/frontend/lowering/collation-repertoire.js +19 -0
  243. package/dist/frontend/lowering/collation-repertoire.js.map +1 -0
  244. package/dist/frontend/lowering/construction-escapes.d.ts +38 -0
  245. package/dist/frontend/lowering/construction-escapes.js +146 -0
  246. package/dist/frontend/lowering/construction-escapes.js.map +1 -0
  247. package/dist/frontend/lowering/construction-escapes.test.d.ts +1 -0
  248. package/dist/frontend/lowering/construction-escapes.test.js +150 -0
  249. package/dist/frontend/lowering/construction-escapes.test.js.map +1 -0
  250. package/dist/frontend/lowering/containers/array-callbacks.js +81 -44
  251. package/dist/frontend/lowering/containers/array-callbacks.js.map +1 -1
  252. package/dist/frontend/lowering/containers/array-construction.js +15 -2
  253. package/dist/frontend/lowering/containers/array-construction.js.map +1 -1
  254. package/dist/frontend/lowering/containers/array-methods.js +231 -12
  255. package/dist/frontend/lowering/containers/array-methods.js.map +1 -1
  256. package/dist/frontend/lowering/containers/callback-arguments.js +43 -1
  257. package/dist/frontend/lowering/containers/callback-arguments.js.map +1 -1
  258. package/dist/frontend/lowering/containers/string-and-regexp.js +7 -63
  259. package/dist/frontend/lowering/containers/string-and-regexp.js.map +1 -1
  260. package/dist/frontend/lowering/cycle-initialization.d.ts +6 -1
  261. package/dist/frontend/lowering/cycle-initialization.js +59 -33
  262. package/dist/frontend/lowering/cycle-initialization.js.map +1 -1
  263. package/dist/frontend/lowering/divergences.d.ts +9 -0
  264. package/dist/frontend/lowering/divergences.js +630 -0
  265. package/dist/frontend/lowering/divergences.js.map +1 -0
  266. package/dist/frontend/lowering/expressions/object-literals.js +40 -2
  267. package/dist/frontend/lowering/expressions/object-literals.js.map +1 -1
  268. package/dist/frontend/lowering/lower-calls.d.ts +5 -0
  269. package/dist/frontend/lowering/lower-calls.js +226 -157
  270. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  271. package/dist/frontend/lowering/lower-classes.d.ts +4 -0
  272. package/dist/frontend/lowering/lower-classes.js +72 -22
  273. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  274. package/dist/frontend/lowering/lower-exprs.d.ts +13 -0
  275. package/dist/frontend/lowering/lower-exprs.js +431 -107
  276. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  277. package/dist/frontend/lowering/lower-generators.d.ts +19 -13
  278. package/dist/frontend/lowering/lower-generators.js +67 -44
  279. package/dist/frontend/lowering/lower-generators.js.map +1 -1
  280. package/dist/frontend/lowering/lower-inspect.js +3 -2
  281. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  282. package/dist/frontend/lowering/lower-island.d.ts +1 -1
  283. package/dist/frontend/lowering/lower-island.js +24 -7
  284. package/dist/frontend/lowering/lower-island.js.map +1 -1
  285. package/dist/frontend/lowering/lower-modules.js +90 -1
  286. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  287. package/dist/frontend/lowering/lower-stmts.js +36 -6
  288. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  289. package/dist/frontend/lowering/lowerer.d.ts +78 -2
  290. package/dist/frontend/lowering/lowerer.js +986 -73
  291. package/dist/frontend/lowering/lowerer.js.map +1 -1
  292. package/dist/frontend/lowering/runtime-optional-bounds.d.ts +19 -0
  293. package/dist/frontend/lowering/runtime-optional-bounds.js +354 -0
  294. package/dist/frontend/lowering/runtime-optional-bounds.js.map +1 -0
  295. package/dist/frontend/lowering/runtime-optional-bounds.test.d.ts +1 -0
  296. package/dist/frontend/lowering/runtime-optional-bounds.test.js +66 -0
  297. package/dist/frontend/lowering/runtime-optional-bounds.test.js.map +1 -0
  298. package/dist/frontend/lowering/runtime-optional-elements.d.ts +117 -0
  299. package/dist/frontend/lowering/runtime-optional-elements.js +738 -0
  300. package/dist/frontend/lowering/runtime-optional-elements.js.map +1 -0
  301. package/dist/frontend/lowering/runtime-optional-guards.d.ts +36 -0
  302. package/dist/frontend/lowering/runtime-optional-guards.js +375 -0
  303. package/dist/frontend/lowering/runtime-optional-guards.js.map +1 -0
  304. package/dist/frontend/lowering/runtime-optional-guards.test.d.ts +1 -0
  305. package/dist/frontend/lowering/runtime-optional-guards.test.js +92 -0
  306. package/dist/frontend/lowering/runtime-optional-guards.test.js.map +1 -0
  307. package/dist/frontend/lowering/runtime-optional-ownership.d.ts +63 -0
  308. package/dist/frontend/lowering/runtime-optional-ownership.js +382 -0
  309. package/dist/frontend/lowering/runtime-optional-ownership.js.map +1 -0
  310. package/dist/frontend/lowering/runtime-optional-provenance.d.ts +2 -1
  311. package/dist/frontend/lowering/runtime-optional-provenance.js +2 -1
  312. package/dist/frontend/lowering/runtime-optional-provenance.js.map +1 -1
  313. package/dist/frontend/lowering/string-index-bounds.d.ts +36 -0
  314. package/dist/frontend/lowering/string-index-bounds.js +537 -0
  315. package/dist/frontend/lowering/string-index-bounds.js.map +1 -0
  316. package/dist/frontend/lowering/string-index-bounds.test.d.ts +1 -0
  317. package/dist/frontend/lowering/string-index-bounds.test.js +223 -0
  318. package/dist/frontend/lowering/string-index-bounds.test.js.map +1 -0
  319. package/dist/frontend/lowering/union-narrow.d.ts +2 -0
  320. package/dist/frontend/lowering/union-narrow.js +10 -6
  321. package/dist/frontend/lowering/union-narrow.js.map +1 -1
  322. package/dist/frontend/program-analysis.d.ts +10 -0
  323. package/dist/frontend/program-analysis.js +7 -0
  324. package/dist/frontend/program-analysis.js.map +1 -1
  325. package/dist/frontend/program.d.ts +25 -0
  326. package/dist/frontend/program.js +613 -17
  327. package/dist/frontend/program.js.map +1 -1
  328. package/dist/frontend/program.test.js +64 -1
  329. package/dist/frontend/program.test.js.map +1 -1
  330. package/dist/frontend/resolve.d.ts +6 -0
  331. package/dist/frontend/resolve.js +14 -0
  332. package/dist/frontend/resolve.js.map +1 -1
  333. package/dist/frontend/threads-import.d.ts +5 -0
  334. package/dist/frontend/threads-import.js +21 -0
  335. package/dist/frontend/threads-import.js.map +1 -0
  336. package/dist/frontend/type-mapper.d.ts +3 -0
  337. package/dist/frontend/type-mapper.js +72 -6
  338. package/dist/frontend/type-mapper.js.map +1 -1
  339. package/dist/frontend/worker-target.d.ts +4 -0
  340. package/dist/frontend/worker-target.js +8 -1
  341. package/dist/frontend/worker-target.js.map +1 -1
  342. package/dist/index.d.ts +6 -1
  343. package/dist/index.js +47 -9
  344. package/dist/index.js.map +1 -1
  345. package/dist/ir/build.d.ts +5 -0
  346. package/dist/ir/build.js +7 -0
  347. package/dist/ir/build.js.map +1 -1
  348. package/dist/ir/builtin-signatures.d.ts +1 -1
  349. package/dist/ir/builtin-signatures.js +3 -0
  350. package/dist/ir/builtin-signatures.js.map +1 -1
  351. package/dist/ir/int32-slots.d.ts +72 -0
  352. package/dist/ir/int32-slots.js +837 -0
  353. package/dist/ir/int32-slots.js.map +1 -0
  354. package/dist/ir/int32-slots.test.d.ts +1 -0
  355. package/dist/ir/int32-slots.test.js +222 -0
  356. package/dist/ir/int32-slots.test.js.map +1 -0
  357. package/dist/ir/integer-ranges.d.ts +19 -3
  358. package/dist/ir/integer-ranges.js +168 -23
  359. package/dist/ir/integer-ranges.js.map +1 -1
  360. package/dist/ir/integer-ranges.test.js +74 -2
  361. package/dist/ir/integer-ranges.test.js.map +1 -1
  362. package/dist/ir/ir.d.ts +24 -6
  363. package/dist/ir/ir.js.map +1 -1
  364. package/dist/ir/validate.js +21 -2
  365. package/dist/ir/validate.js.map +1 -1
  366. package/dist/ir/validate.test.js +39 -0
  367. package/dist/ir/validate.test.js.map +1 -1
  368. package/dist/native/cache.d.ts +3 -0
  369. package/dist/native/cache.js +49 -1
  370. package/dist/native/cache.js.map +1 -1
  371. package/dist/native/cache.test.js +21 -1
  372. package/dist/native/cache.test.js.map +1 -1
  373. package/dist/native/cli.js +1 -1
  374. package/dist/native/cli.js.map +1 -1
  375. package/dist/native/compiler.d.ts +5 -0
  376. package/dist/native/compiler.js +51 -4
  377. package/dist/native/compiler.js.map +1 -1
  378. package/dist/native/prepare.d.ts +4 -0
  379. package/dist/native/prepare.js +2 -1
  380. package/dist/native/prepare.js.map +1 -1
  381. package/dist/startup-cache.d.ts +2 -0
  382. package/dist/startup-cache.js +2 -0
  383. package/dist/startup-cache.js.map +1 -1
  384. package/package.json +20 -20
  385. package/surface-manifest.json +1 -1
@@ -19,6 +19,7 @@ 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";
@@ -32,8 +33,9 @@ import { emitByteWindowGuard, findInitializedByteLoopBindings, matchByteWindow,
32
33
  import { canDeferSplitPiece, emitSplitSpanNext, materializeSplitPiece, emitSplitCursor, emitSplitNext, emitSplitScratch, emitSplitSnapshot, findPrivateSplitLocals, loadSplitSnapshot, storeSplitSnapshot, stringSplit, } from "./split-loops.js";
33
34
  import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
34
35
  import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
35
- import { everyStmtList } from "../../ir/traverse.js";
36
- import { analyzeIntegerRanges } from "../../ir/integer-ranges.js";
36
+ import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
37
+ import { analyzeIntegerRanges, INT32_RANGE } from "../../ir/integer-ranges.js";
38
+ import { analyzeInt32Slots } from "../../ir/int32-slots.js";
37
39
  import { findIntegerViews } from "./integer-views.js";
38
40
  import { findInitializerBindings, withInitializerBindings } from "../../ir/initializer-bindings.js";
39
41
  import { findConstantNumericTables } from "../../ir/constant-tables.js";
@@ -47,10 +49,13 @@ import { allocateLocalUnion, findLocalUnionStorage, storeLocalUnion, } from "./l
47
49
  import { BlockBuilder } from "./blocks.js";
48
50
  import { NullableRefFields } from "./nullable-fields.js";
49
51
  import { emitLocalArrayRead, emitProjectedArrayRead, emitBorrowedArrayRead, findLocalArrayReads, findCallArrayReads, OptionalArrayReads, } from "./local-array-reads.js";
52
+ import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js";
50
53
  import { emitDenseArrayGet, emitDenseArraySet } from "./dense-array-access.js";
51
54
  import { emitStackMapRead, findMapReadLifetimes, matchMapRead, } from "./map-read-lifetimes.js";
52
- import { emitBorrowedFieldSequence } from "./borrowed-receivers.js";
55
+ import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
53
56
  import { emitBorrowedInput } from "./borrowed-inputs.js";
57
+ import { analyzeWalks, findWalkBorrows } from "./walk-borrows.js";
58
+ import { findReboundParameters } from "./rebound-parameters.js";
54
59
  import { ReferenceEffects } from "./reference-effects.js";
55
60
  import { LlvmDebugInfo } from "./debug-info.js";
56
61
  import { StackCallbacks } from "./stack-callbacks.js";
@@ -63,14 +68,16 @@ import { ABSENT_FIELD_PAYLOAD, emitFieldAbsentTest, f64Lit, ffiNativeTypeLl, ffi
63
68
  import { ffiExtendsNarrowIntegers } from "../targets.js";
64
69
  import { emitLiteralExpr, emitOperatorExpr, emitStringExpr, emitContainerExpr, emitRecordExpr, } from "./expr-primitives.js";
65
70
  import { emitControlExpr } from "./expr-control.js";
66
- import { emitCallExpr } from "./expr-calls.js";
71
+ import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
67
72
  import { emitDynamicExpr } from "./expr-dynamic.js";
68
73
  import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js";
74
+ import { PublishWalkers } from "./publish.js";
75
+ import { computePublishedTypes } from "../publication.js";
69
76
  import { emitJsInteropExpr, emitExpr } from "./expr-dispatch.js";
70
77
  import { emitJsMarshal, emitJsOp, emitJsExit, islandAdapter, islandTypedAdapter, } from "./expr-island.js";
71
78
  import { dynKind, raceAdapterFor, genResultThunkFor, childExitThunkFor, childExitSignalThunkFor, childDataThunkFor, execFileThunkFor, ipcMessageThunkFor, ipcSendThunkFor, emitterFixedAdapter, wrapEmitterListener, unwrapNullableClosure, closeBindThunkFor, closeOverrideWrapFor, } from "./expr-callbacks.js";
72
79
  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";
80
+ import { emitStableReceiver, resolveThunkFor, tagInSet, arrPush, emitArrayCopyLoop, emitStrIntrinsic, emitArrIntrinsic, emitDiscardedSplice, wrapNullable, emitMapNew, mapSet, emitMapLikeIntrinsic, emitSetNew, } from "./expr-containers.js";
74
81
  import { emitIntegerLoopIndex, emitBytesIndex, emitBytesData, emitBytesLength, emitBytesGet, emitToUint32, emitBytesSet, emitBytesIntrinsic, } from "./expr-bytes.js";
75
82
  import { emitRegexIntrinsic, emitRecordKeyGet, keyedRecordReadInto } from "./expr-records.js";
76
83
  import { dynPromiseAdapter, streamTypedRefCommitAdapter, liveDynUnionRefAdapter, streamTypedRefBoxValue, streamTypedRefMaterializeAdapter, streamFromArrayAdapter, } from "./expr-stream-bridges.js";
@@ -81,10 +88,16 @@ import { emitNetworkHttpLibCall } from "./lib-network.js";
81
88
  import { emitAssertInspectLibCall, emitIoLibCall, emitGenericLibCall, emitLibCall, } from "./lib-dispatch.js";
82
89
  import { buildClassGraph, classMembershipIntervals, emitClassMembershipHelper, classEnvironmentIndex, classFieldIndex, classStructSym, } from "./classes.js";
83
90
  import { LlDyn } from "./dyn.js";
91
+ import { VirtualBorrows } from "./virtual-borrows.js";
84
92
  import { LlvmUnsupportedError } from "./unsupported.js";
85
93
  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";
94
+ import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js";
95
+ import { emitNullablePresent, emitUnionPeek, emitUnionTag } from "./union-repr.js";
96
+ import { literalCapWord, literalCapWord32 } from "./string-key-hash.js";
97
+ import { NUMBER_MAP_ENTRY_DECL } from "./map-number-lookup.js";
98
+ import { STRING_READ_HELPERS } from "./string-reads.js";
99
+ import { emitConcatInputs, emitMixedConcat, numberPart, stringParts, } from "./string-construction.js";
100
+ import { arrNewCall, boxAccess, boxNewCall, boxReleaseSym, computeTraced, elemAccess, emitInlineRcHelpers, emitNullableRetainWrappers, FN_ATTRS, inlineRcDecls, llFieldType, releaseSym, retainSym, traceArg, typedRefConstructor, vAdapters, } from "./shapes.js";
88
101
  export { LlvmUnsupportedError } from "./unsupported.js";
89
102
  function ffiCallbackDummyLl(callback) {
90
103
  switch (callback.returns) {
@@ -130,6 +143,41 @@ export function emitLlvmModuleSource(mod, options = {}) {
130
143
  function llStrBytes(text) {
131
144
  return llBytes(Buffer.from(text, "utf8"));
132
145
  }
146
+ const NO_BORROWED = new Set();
147
+ /** Whether any node of `e` names the local (reads, writes, declarations,
148
+ * captures). */
149
+ function mentionsLocal(e, localId) {
150
+ const exprNames = (node) => {
151
+ if (node.kind === "varRef" || node.kind === "incDec" || node.kind === "assignExpr")
152
+ return node.localId === localId;
153
+ if (node.kind === "closure")
154
+ return node.captures.includes(localId);
155
+ if (node.kind === "classRef")
156
+ return node.captures?.includes(localId) === true;
157
+ return false;
158
+ };
159
+ const stmtNames = (node) => {
160
+ if (node.kind === "varDecl" ||
161
+ node.kind === "assign" ||
162
+ node.kind === "forOf" ||
163
+ node.kind === "rethrow")
164
+ return node.localId === localId;
165
+ return false;
166
+ };
167
+ let found = false;
168
+ const expr = (node) => {
169
+ if (found || exprNames(node))
170
+ return !(found = true);
171
+ return everyExprChild(node, expr, stmt);
172
+ };
173
+ const stmt = (node) => {
174
+ if (found || stmtNames(node))
175
+ return !(found = true);
176
+ return everyStmtChild(node, expr, stmt);
177
+ };
178
+ expr(e);
179
+ return found;
180
+ }
133
181
  export class LlEmitter {
134
182
  mod;
135
183
  localArrayReads = new Map();
@@ -178,17 +226,23 @@ export class LlEmitter {
178
226
  ffiExtendNarrowIntegers;
179
227
  cycleColorOffset;
180
228
  wasi;
229
+ /** ELF worker executables give thread-locals the executable TLS models. */
230
+ executableTls;
181
231
  emitLibraryIdentity;
182
232
  runtimeAbiMarker;
183
233
  /** Interned string literals: UTF-8 text → { symbol, byte length } —
184
234
  * first-use order, the runtime ABI’s determinism discipline. */
185
235
  literals = new Map();
236
+ /** Symbols of interned constant data, named by content (contentSymbol). */
237
+ contentSymbols = new Set();
186
238
  /** Interned unit-armed union instances: "unionId:tag" → symbol — one
187
239
  * immortal (rc == SIZE_MAX) static per (union, unit tag).
188
240
  * RC entry points and the collector skip immortals. */
189
241
  unitInstances = new Map();
190
242
  /** unionId → symbol of the union's immortal ABSENT field-slot instance. */
191
243
  absentInstances = new Map();
244
+ /** The nullable-union ABSENT sentinel was referenced (absentInstanceRef). */
245
+ needsNullableAbsent = false;
192
246
  immortalValues = new Set();
193
247
  /** Regex literal templates: "<flags>/<pattern>" → { symbol, interned
194
248
  * source/flags literal refs } — one immortal ScrRegex template per distinct
@@ -231,6 +285,7 @@ export class LlEmitter {
231
285
  fnDefText = new Map();
232
286
  referenceEffects;
233
287
  optionalArrayReads;
288
+ checkedNarrows;
234
289
  callArrayReads = new Map();
235
290
  mapReadLifetimes = { locals: new Map(), arguments: new Map() };
236
291
  callLifetimes;
@@ -248,6 +303,18 @@ export class LlEmitter {
248
303
  /** Parameters proven projection-only: callers may pass stack boxes. */
249
304
  projectedParameters = new Set();
250
305
  stableCallBindings = new Set();
306
+ /** Locals and rebound parameters holding borrowed pointers for their
307
+ * whole lifetime (walk-borrows.ts), per function and for the current one. */
308
+ walkBorrowsByFunction = new Map();
309
+ walkBorrows = new Set();
310
+ /** Functions whose borrowing body returns its result without a reference
311
+ * (analyzeBorrowedReturns), and whether the current body is one. */
312
+ borrowedReturns = new Set();
313
+ /** Rebound parameters that keep the borrowed convention with an owner
314
+ * slot (rebound-parameters.ts), per function, and the current owner slots. */
315
+ reboundByFunction = new Map();
316
+ reboundOwnerSlots = new Map();
317
+ currentBorrowedReturn = false;
251
318
  /** Manifest-bound native imports, used by ffiCall emission. */
252
319
  ffiByName = new Map();
253
320
  /** C-ABI callback trampolines and (for raw/no-userdata callbacks) their
@@ -266,6 +333,15 @@ export class LlEmitter {
266
333
  * checks are emitted only after calls that can actually raise. */
267
334
  mayThrow;
268
335
  indirectMayThrow;
336
+ /** Worker programs: the functions that poll for termination on entry
337
+ * (workerTerminationPolls); null outside worker programs. */
338
+ workerEntryPolls;
339
+ /** Functions that check the native stack guard on entry
340
+ * (stackCheckedFunctions); null where the runtime has no guard. */
341
+ stackChecks;
342
+ /** Static types whose values can be published (@scriptc/threads): their
343
+ * stores carry the frozen-object guard. Null without publish calls. */
344
+ publishedTypes;
269
345
  /** Method names with at least one may-throw implementation — the
270
346
  * virtualCall pending check's key. */
271
347
  mayThrowMethods = new Set();
@@ -287,6 +363,10 @@ export class LlEmitter {
287
363
  dynPromiseAdapters = new Map();
288
364
  unionsById = new Map();
289
365
  nullableFields;
366
+ /** Unions whose values are nullable arm pointers (nullable-unions.ts). */
367
+ nullableUnions;
368
+ /** The union table without nullable unions: the stack-box analyses. */
369
+ boxedUnionsById;
290
370
  recordsById = new Map();
291
371
  recordCloneShapes = new Set();
292
372
  tracedShapes;
@@ -294,6 +374,8 @@ export class LlEmitter {
294
374
  /** The class graph (buildClassGraph): preorder numbering, hierarchy
295
375
  * membership, virtual slot lists. */
296
376
  classMeta;
377
+ /** Which vtable slots borrow which parameters (virtual-borrows.ts). */
378
+ virtualBorrows;
297
379
  /** Class objects (classes as first-class values): className → the
298
380
  * interned .name literal ref — registered during body emission, the
299
381
  * statics and construct thunks assemble around the bodies. */
@@ -334,6 +416,8 @@ export class LlEmitter {
334
416
  storedSplits = new Map();
335
417
  streamingSplitsEnabled = false;
336
418
  initializerBindings;
419
+ /** Whole-program int32 storage and parameter/result facts. */
420
+ int32Slots;
337
421
  numericLocals = new Map();
338
422
  captureIds = new Set();
339
423
  /** Active induction bindings retain their width independently of size_t. */
@@ -380,16 +464,23 @@ export class LlEmitter {
380
464
  stmt: () => true,
381
465
  expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
382
466
  }));
467
+ this.nullableUnions = new NullableUnions(mod);
383
468
  this.debug =
384
469
  options.debugSources === undefined
385
470
  ? null
386
- : new LlvmDebugInfo(mod.sourceFile, options.debugSources, options.pointerBits, mod.unions);
471
+ : new LlvmDebugInfo(mod.sourceFile, options.debugSources, options.pointerBits, mod.unions, this.nullableUnions.armTypes());
387
472
  this.constantNumericTables = findConstantNumericTables(mod);
388
473
  this.initializerBindings = findInitializerBindings(mod);
474
+ this.int32Slots = analyzeInt32Slots(mod);
389
475
  this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
390
476
  this.ffiExtendNarrowIntegers =
391
477
  options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
392
478
  this.wasi = options.wasi === true;
479
+ this.executableTls =
480
+ mod.workers === true &&
481
+ mod.lib === undefined &&
482
+ !this.wasi &&
483
+ !/apple|darwin|windows|mingw|cygwin|win32/.test(options.targetTriple || process.platform);
393
484
  this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
394
485
  this.runtimeAbiMarker = options.runtimeAbiMarker === true;
395
486
  this.rcHelpers = options.inlineRc === true ? new Set() : null;
@@ -405,11 +496,17 @@ export class LlEmitter {
405
496
  for (const entry of mod.ffiImports ?? []) {
406
497
  this.ffiByName.set(entry.name, entry);
407
498
  }
408
- const mt = computeMayThrow(mod);
499
+ // Executables on Linux and macOS install a native stack guard per thread
500
+ // and per async fiber; WASI, Windows and library builds have none.
501
+ const triple = (options.targetTriple ?? "").toLowerCase();
502
+ const windows = triple === "" ? process.platform === "win32" : triple.includes("windows");
503
+ const mt = computeMayThrow(mod, {
504
+ stackChecks: !this.wasi && mod.lib === undefined && !windows,
505
+ });
409
506
  this.mayThrow = mt.fns;
410
- if (mod.workers)
411
- for (const fn of mod.functions)
412
- this.mayThrow.add(fn.name);
507
+ this.workerEntryPolls = mt.workerEntryPolls ?? null;
508
+ this.stackChecks = mt.stackChecks ?? null;
509
+ this.publishedTypes = computePublishedTypes(mod);
413
510
  this.indirectMayThrow = mt.indirect || mod.workers === true;
414
511
  for (const cls of mod.classes ?? []) {
415
512
  for (const m of cls.methods ?? []) {
@@ -419,10 +516,56 @@ export class LlEmitter {
419
516
  }
420
517
  for (const u of mod.unions ?? [])
421
518
  this.unionsById.set(u.id, u);
422
- this.nullableFields = new NullableRefFields(mod.classes ?? [], this.unionsById);
519
+ this.boxedUnionsById = this.nullableUnions.boxedUnions(this.unionsById);
520
+ this.immortalValues.add("null");
521
+ this.immortalValues.add(NULLABLE_NULL);
522
+ this.nullableFields = new NullableRefFields(mod.classes ?? [], this.boxedUnionsById);
523
+ // Optional array reads also cover nullable unions: emitLocalArrayRead
524
+ // then yields the (borrowed or owned) element pointer, not a stack box.
423
525
  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);
526
+ this.checkedNarrows = new CheckedNarrows(this.fnByName);
527
+ this.referenceEffects = new ReferenceEffects(this.fnByName, (call) => this.optionalArrayReads.get(call) !== null, mod.classes ?? []);
528
+ const walkParameters = new Map();
529
+ if (this.debug === null) {
530
+ const host = {
531
+ pointerLocal: (local) => this.plainReference(local.type),
532
+ borrowsWithoutOwning: (e) => this.borrowsWithoutOwning(e),
533
+ };
534
+ for (const fn of this.fnByName.values()) {
535
+ if (!this.referenceEffects.functions.has(fn.name))
536
+ continue;
537
+ const walks = findWalkBorrows(fn, host);
538
+ if (walks.size === 0)
539
+ continue;
540
+ this.walkBorrowsByFunction.set(fn.name, walks);
541
+ const indexes = new Set();
542
+ fn.params.forEach((param, index) => {
543
+ if (walks.has(param.localId))
544
+ indexes.add(index);
545
+ });
546
+ if (indexes.size > 0)
547
+ walkParameters.set(fn.name, indexes);
548
+ }
549
+ }
550
+ if (this.debug === null) {
551
+ for (const fn of this.fnByName.values()) {
552
+ // Strings keep the owned parameter: their in-place append
553
+ // (`s = s + x`) needs a uniquely owned binding.
554
+ const rebound = findReboundParameters(fn, (type) => this.plainReference(type) &&
555
+ type.kind !== "string" &&
556
+ (type.kind !== "union" || this.nullableUnions.get(type.unionId)?.arm.kind !== "string"), this.walkBorrowsByFunction.get(fn.name) ?? new Set());
557
+ if (rebound.size === 0)
558
+ continue;
559
+ this.reboundByFunction.set(fn.name, rebound);
560
+ const indexes = walkParameters.get(fn.name) ?? new Set();
561
+ fn.params.forEach((param, index) => {
562
+ if (rebound.has(param.localId))
563
+ indexes.add(index);
564
+ });
565
+ walkParameters.set(fn.name, indexes);
566
+ }
567
+ }
568
+ this.callLifetimes = analyzeCallLifetimes(this.fnByName, (className, field) => this.nullableFields.get(className, field) !== null, walkParameters);
426
569
  this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes);
427
570
  this.stackCallbacks = new StackCallbacks(this.fnByName);
428
571
  this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
@@ -438,6 +581,7 @@ export class LlEmitter {
438
581
  cycleColorOffset: this.cycleColorOffset,
439
582
  tracedShapes: this.tracedShapes,
440
583
  tracedUnions: this.tracedUnions,
584
+ nullableUnions: this.nullableUnions,
441
585
  recordsById: this.recordsById,
442
586
  recordCloneShapes: this.recordCloneShapes,
443
587
  objectAudit: options.objectAudit !== false,
@@ -445,11 +589,11 @@ export class LlEmitter {
445
589
  pointerBits: options.pointerBits,
446
590
  wasi: options.wasi,
447
591
  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,
592
+ threadInstances: mod.lib?.threadInstances === true,
452
593
  }),
594
+ // Worker programs keep one allocator per script thread: the inline
595
+ // paths address its thread-local state.
596
+ threadLocalAlloc: mod.workers === true,
453
597
  rcHelpers: this.rcHelpers,
454
598
  unionsById: this.unionsById,
455
599
  declare: (decl) => this.declare(decl),
@@ -458,6 +602,7 @@ export class LlEmitter {
458
602
  internLiteral: (text) => this.internLiteral(text),
459
603
  cstr: (text) => this.cstr(text),
460
604
  unitInstanceRef: (unionId, tag) => this.unitInstanceRef(unionId, tag),
605
+ fieldAbsentTestIn: (B, value, unionId) => this.fieldAbsentTestIn(B, value, unionId),
461
606
  absentInstanceRef: (unionId) => this.absentInstanceRef(unionId),
462
607
  liveDynRefAdapter: (type) => this.liveDynRefAdapter(type),
463
608
  liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type),
@@ -470,6 +615,8 @@ export class LlEmitter {
470
615
  .filter((meta) => intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post))
471
616
  .map((meta) => meta.def.name);
472
617
  },
618
+ pendingTestLines: (dest) => this.pendingTestLines(dest),
619
+ virtualEntry: (implFn) => this.virtualEntry(implFn),
473
620
  };
474
621
  this.walkers = new LlWalkers(this.shapeHost);
475
622
  this.dyn = new LlDyn(this.shapeHost);
@@ -526,6 +673,21 @@ export class LlEmitter {
526
673
  lib: rec.lib,
527
674
  });
528
675
  }
676
+ this.virtualBorrows = new VirtualBorrows(mod, this.classMeta, this.fnByName, this.callLifetimes.borrowed);
677
+ // After the vtable slots: borrowing slots exclude their implementations.
678
+ if (this.debug === null)
679
+ this.analyzeBorrowedReturns();
680
+ }
681
+ /** The borrowed parameters of a hierarchy's vtable slot (empty: owned). */
682
+ virtualSlotBorrowed(rootName, slotIndex) {
683
+ return this.virtualBorrows.slot(rootName, slotIndex);
684
+ }
685
+ /** The vtable entry for one implementation (VirtualBorrows.entry). */
686
+ virtualEntry(implFn) {
687
+ return this.virtualBorrows.entry(implFn);
688
+ }
689
+ implSlotBorrowed(implFn) {
690
+ return this.virtualBorrows.implSlotBorrowed(implFn);
529
691
  }
530
692
  abiOffset(native64, wasm32) {
531
693
  return this.sizeType === "i32" ? wasm32 : native64;
@@ -566,7 +728,6 @@ export class LlEmitter {
566
728
  const defs = [];
567
729
  if (this.ffiCallbackAdapters.size === 0)
568
730
  return { globals, defs };
569
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
570
731
  this.declare(`declare void @scr_trap(ptr)`);
571
732
  const expired = this.cstr("scriptc: native callback invoked outside its call-scoped lifetime\n");
572
733
  const released = this.cstr("scriptc: native callback invoked outside its retained lifetime\n");
@@ -684,7 +845,7 @@ export class LlEmitter {
684
845
  ? ` %cb = load ptr, ptr @${adapter.tls}`
685
846
  : adapter.global !== null
686
847
  ? ` %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:`);
848
+ : ` %cb = getelementptr inbounds i8, ptr %ctx, i64 0`, ` %missing = icmp eq ptr %cb, null`, ` br i1 %missing, label %expired, label %ready`, `expired:`, ` call void @scr_trap(ptr ${adapter.callback.lifetime === "call" ? expired : released})`, ` unreachable`, `ready:`, ...this.pendingTestLines("%pending"), ` br i1 %pending, label %skip, label %invoke`, `skip:`, ` ret ${ffiCallbackDummyLl(cb)}`, `invoke:`);
688
849
  // Validate every native pointer before allocating any copy-in value.
689
850
  // A later bad slot therefore cannot leak an earlier materialization.
690
851
  for (let i = 0; i < cb.params.length; i++) {
@@ -832,6 +993,20 @@ export class LlEmitter {
832
993
  return this.emitParts().join("\n");
833
994
  }
834
995
  emitParts() {
996
+ const parts = this.emitModuleParts();
997
+ if (!this.executableTls)
998
+ return parts;
999
+ // A worker executable's thread-locals all live in the executable's own
1000
+ // TLS block (the program's internal ones, the runtime's external ones),
1001
+ // so on ELF they can use the executable models: LLVM's default
1002
+ // general-dynamic sequence (relaxed by the linker to `mov %fs:0` plus
1003
+ // an add) becomes one %fs-relative access LLVM can fold into loads.
1004
+ // Darwin's thread-local variables have a single model.
1005
+ return parts.map((part) => part
1006
+ .replace(/^(@[-$._A-Za-z0-9]+ = internal )thread_local global /gm, "$1thread_local(localexec) global ")
1007
+ .replace(/^(@[-$._A-Za-z0-9]+ = external )thread_local global /gm, "$1thread_local(initialexec) global "));
1008
+ }
1009
+ emitModuleParts() {
835
1010
  // Function bodies first (the literal/unit/fn-value tables fill as they
836
1011
  // emit), then the file assembles around them — the runtime ABI’s order.
837
1012
  const fnDefs = [];
@@ -858,7 +1033,7 @@ export class LlEmitter {
858
1033
  this.declare(`declare void @scr_error_register_dyn(ptr, ptr)`);
859
1034
  errorViews.push(` call void @scr_error_register_dyn(ptr @${mangleVtInstance(meta.def.name)}, ptr @${name})`);
860
1035
  }
861
- const layouts = emitLlvmLayouts(this.shapeHost, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t), this.nullableFields);
1036
+ const layouts = emitLlvmLayouts(this.shapeHost, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t), this.nullableFields, this.int32Slots);
862
1037
  const shapes = layouts.records;
863
1038
  const classShapes = layouts.classes;
864
1039
  const classObjDefs = layouts.classObjects;
@@ -1061,12 +1236,15 @@ export class LlEmitter {
1061
1236
  const topPendingReleases = asyncEntry ? globalReleaseLines("gp") : [];
1062
1237
  const loopReportedReleases = runsLoop ? globalReleaseLines("gq") : [];
1063
1238
  // 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).
1239
+ // HERE, before the extern block flushes. They run once, so they keep
1240
+ // the out-of-line scr_exc_pending call; per-call checks inline the
1241
+ // test (pendingTestLines).
1066
1242
  if (entryMayThrow || runsLoop)
1067
1243
  this.declare(`declare zeroext i1 @scr_exc_pending()`);
1068
1244
  if (runsLoop) {
1069
1245
  this.declare(`declare zeroext i1 @scr_loop_run(ptr)`);
1246
+ if (this.mod.commonJsEntry === true)
1247
+ this.declare(`declare void @scr_loop_commonjs_entry()`);
1070
1248
  this.declare(`declare zeroext i1 @scr_report_unhandled_rejections()`);
1071
1249
  this.declare(`declare void @scr_discard_unhandled_rejections()`);
1072
1250
  }
@@ -1157,6 +1335,8 @@ export class LlEmitter {
1157
1335
  const helpers = [
1158
1336
  ...this.helperDefs(),
1159
1337
  ...emitClassMembershipHelper(this.classMeta, this.sizeType),
1338
+ ...(this.publishWalkersCache?.defs() ?? []),
1339
+ ...this.publishedClassNamesDefs(),
1160
1340
  ];
1161
1341
  if (this.constantNumericTables.size > 0)
1162
1342
  this.declare(`declare double @scr_arr_get_number(ptr, double)`);
@@ -1186,6 +1366,15 @@ export class LlEmitter {
1186
1366
  ...(this.rcHelpers === null
1187
1367
  ? []
1188
1368
  : [`%ScrMapRc = type { ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr, ptr, ptr }`]),
1369
+ // The complete ScrMap and its { key, val, hash } entry, read by the
1370
+ // inline number-key lookup (sc_map_entry_f64): entries is field 12,
1371
+ // the direct index and its length fields 18 and 19.
1372
+ ...(this.decls.has(NUMBER_MAP_ENTRY_DECL)
1373
+ ? [
1374
+ `%ScrMapIx = type { ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ${this.sizeType}, ptr, ${this.sizeType}, ptr, ptr, ptr, ${this.sizeType} }`,
1375
+ `%ScrMapEntry = type { i64, i64, i64 }`,
1376
+ ]
1377
+ : []),
1189
1378
  // The runtime error prefix { rc, vt, name, message, code, cause } and the
1190
1379
  // class-object shape { rc, pre, post, ctor, name } — field reads on
1191
1380
  // builtin errors and classval loads GEP through these.
@@ -1231,7 +1420,7 @@ export class LlEmitter {
1231
1420
  // data (string literals, unit arms, template arrays, vtables) stays
1232
1421
  // shared.
1233
1422
  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
1423
+ out.push(``, `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, `declare void @scr_init()`, `declare void @scr_lib_init(i32, ptr)`, ...(this.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []), ...(this.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []), ...(this.runtimeAbiMarker && this.mod.lib === undefined
1235
1424
  ? [`declare void @${RUNTIME_ABI_MARKER}()`]
1236
1425
  : []));
1237
1426
  for (const d of this.decls)
@@ -1250,7 +1439,10 @@ export class LlEmitter {
1250
1439
  // the runtime ABI’s static table, retain/release skip rc == SIZE_MAX.
1251
1440
  // 64-bit (little-endian) targets split the capacity word: the high
1252
1441
  // 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);
1442
+ // Bit 31 marks all-ASCII literals (SCR_STR_ASCII_BIT) on every target.
1443
+ const capWord = this.sizeType === "i64"
1444
+ ? literalCapWord(Buffer.from(text, "utf8"))
1445
+ : literalCapWord32(Buffer.from(text, "utf8"));
1254
1446
  out.push(`@${lit.sym} = internal global { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, [${lit.len + 1} x i8] } ` +
1255
1447
  `{ ${this.sizeType} -1, ${this.sizeType} ${lit.len}, ${this.sizeType} ${capWord}, [${lit.len + 1} x i8] c"${llStrBytes(text)}" }`);
1256
1448
  }
@@ -1276,6 +1468,20 @@ export class LlEmitter {
1276
1468
  }
1277
1469
  if (this.absentInstances.size > 0)
1278
1470
  out.push(``);
1471
+ if (this.nullableUnions.usesNullSentinel) {
1472
+ // `null` in every `C | null | undefined` union (NULL is undefined):
1473
+ // one module-scope immortal object like the ABSENT sentinel.
1474
+ const S = this.sizeType;
1475
+ out.push(`${NULLABLE_NULL} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable null arm`, ``);
1476
+ }
1477
+ if (this.needsNullableAbsent) {
1478
+ // The ABSENT field-slot state of every nullable-pointer union: one
1479
+ // module-scope immortal object (rc == SIZE_MAX), distinct from NULL
1480
+ // (undefined) and from every instance. RC entry points and the
1481
+ // collector skip it like any immortal; field reads compare its address.
1482
+ const S = this.sizeType;
1483
+ out.push(`${NULLABLE_ABSENT} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable absent field`, ``);
1484
+ }
1279
1485
  for (const [key, re] of this.regexInstances) {
1280
1486
  // One immortal ScrRegex per (pattern, flags) literal, pointing at
1281
1487
  // the interned source/flags strings. The bc and native-matcher slots
@@ -1442,7 +1648,10 @@ export class LlEmitter {
1442
1648
  out.push(this.debug.render());
1443
1649
  return out;
1444
1650
  }
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,
1651
+ out.push(`define i32 @${this.mod.workers ? "sc_context_entry" : this.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`, `entry:`, ...(this.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []), ...(this.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []), ` call void @scr_init()`,
1652
+ // Each context (the main thread, every worker) installs its stack
1653
+ // guard before running compiled code that checks it.
1654
+ ...(this.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []), ...stamps,
1446
1655
  // Event-surface programs (signal/exit listeners) fill the loop's
1447
1656
  // nullable event hooks before %main — scr_events.c links only when
1448
1657
  // this line is emitted (native/executable.ts gates on the same predicate).
@@ -1487,6 +1696,7 @@ export class LlEmitter {
1487
1696
  // both exit 1, like Node — the C main's loop block exactly.
1488
1697
  ...(runsLoop
1489
1698
  ? [
1699
+ ...(this.mod.commonJsEntry === true ? [` call void @scr_loop_commonjs_entry()`] : []),
1490
1700
  ` %loop_rejection = call zeroext i1 @scr_loop_run(ptr ${asyncEntry ? "%top" : "null"})`,
1491
1701
  ` %lexc = call zeroext i1 @scr_exc_pending()`,
1492
1702
  ` br i1 %lexc, label %luncaught, label %lok`,
@@ -1802,10 +2012,19 @@ export class LlEmitter {
1802
2012
  msgHelper("sc_bad_key", "sc_bad_key_msg", "scriptc: TypeError: record has no key (typed slot — no undefined is representable)\n");
1803
2013
  }
1804
2014
  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);
2015
+ // scr_box_retain is a static inline (increment-unless-immortal);
1807
2016
  // 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`, `}`, ``);
2017
+ defs.push(`define internal ptr @sc_retain_box(ptr %b) ${FN_ATTRS} {`, `entry:`, ` %rc = load ${this.sizeType}, ptr %b`, ` %imm = icmp eq ${this.sizeType} %rc, -1`, ` br i1 %imm, label %done, label %inc`, `inc:`, ` %n = add ${this.sizeType} %rc, 1`, ` store ${this.sizeType} %n, ptr %b`, ` br label %done`, `done:`, ` ret ptr %b`, `}`, ``);
2018
+ }
2019
+ // Number-key Map/Set lookups probe the runtime's direct index inline:
2020
+ // while ndense is nonzero every live key is an integer below it, so an
2021
+ // in-range integral key reads its slot (entry index + 1, 0 = absent) and
2022
+ // any other key is absent. Maps without the index (ndense 0) and every
2023
+ // non-integral probe outside the table take the runtime lookup, which
2024
+ // keeps the complete SameValueZero semantics.
2025
+ if (this.decls.has(NUMBER_MAP_ENTRY_DECL)) {
2026
+ const sz = this.sizeType;
2027
+ defs.push(`define internal ptr @sc_map_entry_f64(ptr %m, double %k) ${FN_ATTRS} {`, `entry:`, ` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 19`, ` %nd = load ${sz}, ptr %ndp`, ` %ndf = uitofp ${sz} %nd to double`, ` %lo = fcmp oge double %k, 0.0`, ` %hi = fcmp olt double %k, %ndf`, ` %in = and i1 %lo, %hi`, ` br i1 %in, label %direct, label %outside`, `direct:`, ` %i = fptoui double %k to ${sz}`, ` %back = uitofp ${sz} %i to double`, ` %int = fcmp oeq double %back, %k`, ` br i1 %int, label %slot, label %miss`, `slot:`, ` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 18`, ` %dense = load ptr, ptr %dp`, ` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`, ` %s = load i32, ptr %sp`, ` %empty = icmp eq i32 %s, 0`, ` br i1 %empty, label %miss, label %hit`, `hit:`, ` %e32 = sub i32 %s, 1`, sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`, ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 12`, ` %entries = load ptr, ptr %ep`, ` %found = getelementptr inbounds %ScrMapEntry, ptr %entries, ${sz} %e`, ` ret ptr %found`, `miss:`, ` ret ptr null`, `outside:`, ` %dense.any = icmp ne ${sz} %nd, 0`, ` br i1 %dense.any, label %miss, label %slow`, `slow:`, ` %r = call ptr @scr_map_entry_f64(ptr %m, double %k)`, ` ret ptr %r`, `}`, ``);
1809
2028
  }
1810
2029
  // String comparisons enter through small internal wrappers that LLVM
1811
2030
  // inlines at each site: interned literals and shared values compare by
@@ -1814,6 +2033,15 @@ export class LlEmitter {
1814
2033
  if (this.decls.has(`declare zeroext i1 @scr_str_eq(ptr, ptr)`)) {
1815
2034
  defs.push(`define internal zeroext i1 @sc_str_eq(ptr %a, ptr %b) ${FN_ATTRS} {`, `entry:`, ` %same = icmp eq ptr %a, %b`, ` br i1 %same, label %yes, label %lengths`, `lengths:`, ` %alen.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 1`, ` %blen.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 1`, ` %alen = load ${this.sizeType}, ptr %alen.p`, ` %blen = load ${this.sizeType}, ptr %blen.p`, ` %samelen = icmp eq ${this.sizeType} %alen, %blen`, ` br i1 %samelen, label %bytes, label %no`, `bytes:`, ` %eq = call zeroext i1 @scr_str_eq(ptr %a, ptr %b)`, ` ret i1 %eq`, `yes:`, ` ret i1 true`, `no:`, ` ret i1 false`, `}`, ``);
1816
2035
  }
2036
+ // String length and charCodeAt read proven-ASCII strings inline: bit 31
2037
+ // of the capacity word (SCR_STR_ASCII_BIT, the low half's sign bit on
2038
+ // little-endian targets) makes UTF-16 units bytes. Unproven strings and
2039
+ // anything but an in-range index take the runtime's UTF-16 mapping.
2040
+ for (const helper of STRING_READ_HELPERS) {
2041
+ if (!this.decls.has(helper.decl))
2042
+ continue;
2043
+ defs.push(...helper.define(this.sizeType, FN_ATTRS), ``);
2044
+ }
1817
2045
  // Ordering also decides inline when the first bytes differ: byte order
1818
2046
  // is the plain comparison's order, and the UTF-16 order whenever both
1819
2047
  // bytes are ASCII. Callers only test the sign of the result.
@@ -1861,7 +2089,18 @@ export class LlEmitter {
1861
2089
  * here too. */
1862
2090
  inlineRcTail(flushedDecls) {
1863
2091
  const late = inlineRcDecls(this.shapeHost).filter((d) => !flushedDecls.has(d));
1864
- return [...late, ...emitInlineRcHelpers(this.shapeHost)];
2092
+ // NULL-skipping retain wrappers of nullable unions; a runtime inner
2093
+ // retain requested after the extern block flushed is declared here.
2094
+ for (const inner of this.nullableUnions.retainWrappers.values()) {
2095
+ const decl = `declare ptr ${inner}(ptr)`;
2096
+ if (inner.startsWith("@scr_") && !flushedDecls.has(decl) && !late.includes(decl))
2097
+ late.push(decl);
2098
+ }
2099
+ return [
2100
+ ...late,
2101
+ ...emitNullableRetainWrappers(this.shapeHost),
2102
+ ...emitInlineRcHelpers(this.shapeHost),
2103
+ ];
1865
2104
  }
1866
2105
  /** Env-signature wrappers + interned immortal closures for declared
1867
2106
  * functions used as values (the runtime ABI’s sc_w_/sc_fc_ pair): every
@@ -1892,7 +2131,6 @@ export class LlEmitter {
1892
2131
  const sizeOf = `ptrtoint (ptr getelementptr (%${pack}, ptr null, i32 1) to ${this.sizeType})`;
1893
2132
  this.declare(`declare void @free(ptr)`);
1894
2133
  this.declare(`declare ptr @malloc(${this.sizeType})`);
1895
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
1896
2134
  const tr = [
1897
2135
  `define internal void @${mangleTrampoline(fn.name)}(ptr %self, ptr %ap) ${FN_ATTRS} {`,
1898
2136
  `entry:`,
@@ -2048,7 +2286,6 @@ export class LlEmitter {
2048
2286
  this.declare(`declare void @scr_async_inline_enter(ptr)`);
2049
2287
  this.declare(`declare ptr @scr_async_inline_leave(ptr)`);
2050
2288
  this.declare(`declare void @scr_promise_reject_pending(ptr)`);
2051
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
2052
2289
  if (lifted) {
2053
2290
  this.declare(`declare ptr @scr_closure_retain_v(ptr)`);
2054
2291
  this.declare(`declare void @scr_closure_release(ptr)`);
@@ -2064,7 +2301,7 @@ export class LlEmitter {
2064
2301
  ` call void @scr_async_inline_enter(ptr %frame)`,
2065
2302
  retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
2066
2303
  ...(lifted ? [` call void @scr_closure_release(ptr %a0)`] : []),
2067
- ` %pend = call zeroext i1 @scr_exc_pending()`,
2304
+ ...this.pendingTestLines("%pend"),
2068
2305
  ` %p = call ptr @scr_async_inline_leave(ptr %frame)`,
2069
2306
  ` br i1 %pend, label %thrown, label %clean`,
2070
2307
  `clean:`,
@@ -2123,7 +2360,7 @@ export class LlEmitter {
2123
2360
  if (fn.captures !== undefined) {
2124
2361
  this.declare(`declare void @scr_closure_release(ptr)`);
2125
2362
  }
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)`);
2363
+ tr.push(...this.pendingTestLines("%pend"), ` br i1 %pend, label %thrown, label %clean`, `clean:`, ` %pr = call ptr @scr_fiber_promise(ptr %self)`);
2127
2364
  for (const line of this.asyncFulfillLines(ret, "%pr"))
2128
2365
  tr.push(line);
2129
2366
  tr.push(` ret void`, `thrown:`);
@@ -2232,7 +2469,7 @@ export class LlEmitter {
2232
2469
  tr.push(` ret void`, `}`, ``);
2233
2470
  }
2234
2471
  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:`);
2472
+ tr.push(` %g = call ptr @scr_gen_of_fiber(ptr %self)`, ...this.pendingTestLines("%pend"), ` br i1 %pend, label %thrown, label %clean`, `clean:`);
2236
2473
  switch (ret.kind) {
2237
2474
  case "void":
2238
2475
  tr.push(` br label %done ; void body: the done value is undefined (NONE)`);
@@ -2322,10 +2559,30 @@ export class LlEmitter {
2322
2559
  return out;
2323
2560
  }
2324
2561
  // ── plumbing (frame and scope ownership) ──────
2562
+ /** A symbol derived from the constant's text rather than its first-use
2563
+ * index, so an edit that interns one more string leaves every other
2564
+ * constant's symbol, and the code referencing it, unchanged. This lets
2565
+ * native builds reuse the compiled partitions an edit does not touch. A
2566
+ * collision retries with a counter, still in first-use order. */
2567
+ contentSymbol(prefix, text) {
2568
+ for (let attempt = 0;; attempt++) {
2569
+ const digest = createHash("sha256")
2570
+ .update(attempt === 0 ? text : `${attempt}\0${text}`)
2571
+ .digest("hex");
2572
+ const symbol = `${prefix}${parseInt(digest.slice(0, 13), 16)}`;
2573
+ if (!this.contentSymbols.has(symbol)) {
2574
+ this.contentSymbols.add(symbol);
2575
+ return symbol;
2576
+ }
2577
+ }
2578
+ }
2325
2579
  internLiteral(text) {
2326
2580
  let lit = this.literals.get(text);
2327
2581
  if (!lit) {
2328
- lit = { sym: `sc_lit_${this.literals.size}`, len: Buffer.byteLength(text, "utf8") };
2582
+ lit = {
2583
+ sym: this.contentSymbol("sc_lit_", text),
2584
+ len: Buffer.byteLength(text, "utf8"),
2585
+ };
2329
2586
  this.literals.set(text, lit);
2330
2587
  }
2331
2588
  const name = `@${lit.sym}`;
@@ -2337,7 +2594,7 @@ export class LlEmitter {
2337
2594
  cstr(text) {
2338
2595
  let c = this.cstrs.get(text);
2339
2596
  if (!c) {
2340
- c = { sym: `sc_cs_${this.cstrs.size}`, len: Buffer.byteLength(text, "utf8") };
2597
+ c = { sym: this.contentSymbol("sc_cs_", text), len: Buffer.byteLength(text, "utf8") };
2341
2598
  this.cstrs.set(text, c);
2342
2599
  }
2343
2600
  return `@${c.sym}`;
@@ -2354,6 +2611,12 @@ export class LlEmitter {
2354
2611
  if (!arm || !isUnitType(arm)) {
2355
2612
  throw new InternalCompilerError(`llvm emitter bug: unit instance for non-unit arm ${tag} of ${unionId}`);
2356
2613
  }
2614
+ const nullable = this.nullableUnions.get(unionId);
2615
+ if (nullable) {
2616
+ // NULL is the unit arm of a nullable union (`undefined` when it has
2617
+ // both); the `null` sentinel is immortal. Neither owns anything.
2618
+ return tag === nullable.nullTag ? NULLABLE_NULL : "null";
2619
+ }
2357
2620
  const key = `${unionId}:${tag}`;
2358
2621
  let sym = this.unitInstances.get(key);
2359
2622
  if (!sym) {
@@ -2370,6 +2633,11 @@ export class LlEmitter {
2370
2633
  if (undefinedArmTag({ kind: "union", unionId }, this.unionsById) < 0) {
2371
2634
  throw new InternalCompilerError(`llvm emitter bug: absent state of ${unionId} without undefined`);
2372
2635
  }
2636
+ if (this.nullableUnions.has(unionId)) {
2637
+ this.needsNullableAbsent = true;
2638
+ this.immortalValues.add(NULLABLE_ABSENT);
2639
+ return NULLABLE_ABSENT;
2640
+ }
2373
2641
  let sym = this.absentInstances.get(unionId);
2374
2642
  if (!sym) {
2375
2643
  sym = `sc_absent_${this.absentInstances.size}`;
@@ -2382,7 +2650,18 @@ export class LlEmitter {
2382
2650
  /** i1: a union value loaded from a field slot is the ABSENT state — the
2383
2651
  * undefined arm's tag with the absent payload marker. */
2384
2652
  fieldAbsentTest(value, unionId) {
2385
- return emitFieldAbsentTest(this.B, value, undefinedArmTag({ kind: "union", unionId }, this.unionsById));
2653
+ return this.fieldAbsentTestIn(this.B, value, unionId);
2654
+ }
2655
+ /** fieldAbsentTest in another block builder (helper generators). A
2656
+ * nullable union's ABSENT state is the module's immortal sentinel. */
2657
+ fieldAbsentTestIn(B, value, unionId) {
2658
+ if (this.nullableUnions.has(unionId)) {
2659
+ this.needsNullableAbsent = true;
2660
+ const t = B.tmp();
2661
+ B.line(`${t} = icmp eq ptr ${value}, ${NULLABLE_ABSENT}`);
2662
+ return t;
2663
+ }
2664
+ return emitFieldAbsentTest(B, value, undefinedArmTag({ kind: "union", unionId }, this.unionsById));
2386
2665
  }
2387
2666
  /** Loads a record field slot as a VALUE: an undefined-armed union slot
2388
2667
  * holding the ABSENT state surfaces as the ordinary undefined arm, so
@@ -2410,9 +2689,20 @@ export class LlEmitter {
2410
2689
  throw new InternalCompilerError("llvm emitter bug: no active statement frame");
2411
2690
  return frame;
2412
2691
  }
2692
+ /** Whether a value is immortal: an interned constant, or a temp marked by
2693
+ * markImmortal. */
2694
+ isImmortal(name) {
2695
+ return this.immortalValues.has(name) || this.B.immortalTemps.has(name);
2696
+ }
2697
+ /** Record that a temp holds an immortal value (a choice between interned
2698
+ * literals): nothing owns it, and retains, releases and moves skip it. */
2699
+ markImmortal(v) {
2700
+ this.B.immortalTemps.add(v.name);
2701
+ return v;
2702
+ }
2413
2703
  /** Registers an owned refcounted value on the current statement frame. */
2414
2704
  own(v) {
2415
- if (isRefCounted(v.type) && !this.immortalValues.has(v.name))
2705
+ if (isRefCounted(v.type) && !this.isImmortal(v.name))
2416
2706
  this.currentFrame().push(v);
2417
2707
  return v;
2418
2708
  }
@@ -2424,7 +2714,7 @@ export class LlEmitter {
2424
2714
  }
2425
2715
  /** Strike a refcounted temp from its frame: ownership is being moved. */
2426
2716
  moveTemp(v) {
2427
- if (!isRefCounted(v.type) || this.immortalValues.has(v.name))
2717
+ if (!isRefCounted(v.type) || this.isImmortal(v.name))
2428
2718
  return;
2429
2719
  for (let i = this.frames.length - 1; i >= 0; i--) {
2430
2720
  const idx = this.frames[i].findIndex((e) => e.name === v.name);
@@ -2438,7 +2728,7 @@ export class LlEmitter {
2438
2728
  /** The retained (+1) read of a refcounted value — type-directed through
2439
2729
  * the `_v` table (immortals skip, exactly the C retain calls). */
2440
2730
  retainValue(name, type) {
2441
- if (this.immortalValues.has(name))
2731
+ if (this.isImmortal(name))
2442
2732
  return name;
2443
2733
  const t = this.B.tmp();
2444
2734
  this.B.line(`${t} = call ptr ${retainSym(this.shapeHost, type)}(ptr ${name})`);
@@ -2447,7 +2737,7 @@ export class LlEmitter {
2447
2737
  /** The release call for one owned refcounted value — type-directed like
2448
2738
  * releaseCallC (all runtime releases are NULL-tolerant). */
2449
2739
  releaseValue(name, type) {
2450
- if (this.immortalValues.has(name))
2740
+ if (this.isImmortal(name))
2451
2741
  return;
2452
2742
  this.B.line(`call void ${releaseSym(this.shapeHost, type)}(ptr ${name})`);
2453
2743
  }
@@ -2472,7 +2762,21 @@ export class LlEmitter {
2472
2762
  for (const e of scope) {
2473
2763
  const t = this.B.tmp();
2474
2764
  this.B.line(`${t} = load ptr, ptr ${e.slot}`);
2475
- if (e.boxed) {
2765
+ if (e.boxed && e.lazy) {
2766
+ // Unreachable code after a terminator emits nothing; a new block
2767
+ // here would revive it.
2768
+ if (this.B.isTerminated())
2769
+ continue;
2770
+ const created = this.B.tmp();
2771
+ this.B.line(`${created} = icmp ne ptr ${t}, null`);
2772
+ const release = this.B.newLabel("lazy.release"), join = this.B.newLabel("lazy.release.j");
2773
+ this.B.condBr(created, release, join);
2774
+ this.B.startBlock(release);
2775
+ this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`);
2776
+ this.B.br(join);
2777
+ this.B.startBlock(join);
2778
+ }
2779
+ else if (e.boxed) {
2476
2780
  this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`);
2477
2781
  }
2478
2782
  else {
@@ -2523,8 +2827,11 @@ export class LlEmitter {
2523
2827
  * function — and branch to the handler / return a dummy value (never
2524
2828
  * read: callers of a may-throw function test the pending flag before
2525
2829
  * using the result). Callers own the surrounding pending branch; a
2526
- * `throw` unwinds unconditionally. */
2527
- emitUnwind() {
2830
+ * `throw` unwinds unconditionally. `passThrough` names a scalar call
2831
+ * result of the function's own return type: with nothing to release, the
2832
+ * unwind returns it as the dummy, so a call in tail position keeps a single
2833
+ * return and stays eligible for tail-call and recursion elimination. */
2834
+ emitUnwind(passThrough) {
2528
2835
  if (this.mod.workers && this.tryStack.length > 0) {
2529
2836
  this.declare(`declare zeroext i1 @scr_context_stopping()`);
2530
2837
  const stopped = this.B.tmp();
@@ -2574,6 +2881,11 @@ export class LlEmitter {
2574
2881
  }
2575
2882
  }
2576
2883
  if (entries.length === 0) {
2884
+ if (passThrough !== undefined && !target && this.currentWasiCoro === null) {
2885
+ const t = this.llType(this.currentReturnType);
2886
+ if (t === "double" || t === "i1")
2887
+ terminator = `ret ${t} ${passThrough}`;
2888
+ }
2577
2889
  this.B.terminate(terminator);
2578
2890
  return;
2579
2891
  }
@@ -2584,24 +2896,147 @@ export class LlEmitter {
2584
2896
  }
2585
2897
  this.B.br(cleanup.label);
2586
2898
  }
2899
+ /** Instructions (two-space indented) that set the i1 `dest` to "an
2900
+ * exception is pending in the active context" — scr_exc_pending()'s
2901
+ * meaning — for the helper and adapter bodies that test it outside
2902
+ * emitPendingCheck. Executables read the runtime's exported active-cell
2903
+ * pointer and compare its i32 kind (offset 0) against SCR_EXC_NONE inline,
2904
+ * as emitPendingCheck does; the out-of-line call would be opaque to LLVM
2905
+ * because the runtime links as prebuilt objects. Libraries and worker
2906
+ * programs keep the runtime call, which is emitPendingCheck's path there
2907
+ * too (workers also observe cross-thread termination in it). */
2908
+ pendingTestLines(dest) {
2909
+ if (this.mod.lib !== undefined || this.mod.workers === true) {
2910
+ this.declare(`declare zeroext i1 @scr_exc_pending()`);
2911
+ return [` ${dest} = call zeroext i1 @scr_exc_pending()`];
2912
+ }
2913
+ this.declare(`@scr_exc_active = external global ptr`);
2914
+ return [
2915
+ ` ${dest}.cell = load ptr, ptr @scr_exc_active`,
2916
+ ` ${dest}.kind = load i32, ptr ${dest}.cell`,
2917
+ ` ${dest} = icmp ne i32 ${dest}.kind, 0`,
2918
+ ];
2919
+ }
2587
2920
  /** The emitter contract for exceptions: after EVERY call that can throw
2588
2921
  * (per the may-throw analysis), test the pending flag and unwind. The
2589
2922
  * 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() {
2923
+ * releases the dummy (NULL for refcounted kinds) harmlessly. A scalar
2924
+ * call result of the function's return type may be passed as the unwind's
2925
+ * dummy value (see emitUnwind). */
2926
+ emitPendingCheck(passThrough) {
2927
+ const through = passThrough !== undefined &&
2928
+ this.llType(passThrough.type) === this.llType(this.currentReturnType) &&
2929
+ (passThrough.type.kind === "f64" || passThrough.type.kind === "bool")
2930
+ ? passThrough.name
2931
+ : undefined;
2592
2932
  const B = this.B;
2593
2933
  if (B.isTerminated())
2594
2934
  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()`);
2935
+ if (this.mod.lib !== undefined) {
2936
+ this.declare(`declare zeroext i1 @scr_exc_pending()`);
2937
+ const p = B.tmp();
2938
+ B.line(`${p} = call zeroext i1 @scr_exc_pending()`);
2939
+ const lu = B.newLabel("exc.u");
2940
+ const lk = B.newLabel("exc.k");
2941
+ B.condBr(p, lu, lk);
2942
+ B.startBlock(lu);
2943
+ this.emitUnwind(through);
2944
+ B.startBlock(lk);
2945
+ return;
2946
+ }
2947
+ // Executables test the active exception cell's kind inline (its first
2948
+ // field). Worker programs test the process-wide alert word instead: it
2949
+ // is nonzero while any script thread may have an exception pending in
2950
+ // its active cell or has been asked to stop, so the common case is one
2951
+ // ordinary global load (a thread-local access is a call on Darwin) and
2952
+ // the out-of-line scr_exc_pending answers for this thread, observing
2953
+ // cancellation and reinstalling the termination sentinel.
2954
+ this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
2955
+ let word;
2956
+ if (this.mod.workers === true) {
2957
+ this.declare(`@scr_exc_alert = external global i32, align 64`);
2958
+ word = B.tmp();
2959
+ B.line(`${word} = load atomic i32, ptr @scr_exc_alert monotonic, align 64`);
2960
+ }
2961
+ else {
2962
+ this.declare(`@scr_exc_active = external global ptr`);
2963
+ let cell;
2964
+ if (B.excCellInvariant) {
2965
+ // Fiber switches restore the active cell before control returns to a
2966
+ // synchronous frame, so its pointer is one value for the whole call:
2967
+ // load it once in the entry block and test only the kind here.
2968
+ if (B.excCell === null) {
2969
+ B.excCell = "%exc.cell";
2970
+ B.entryAllocas.push(`${B.excCell} = load ptr, ptr @scr_exc_active`);
2971
+ }
2972
+ cell = B.excCell;
2973
+ }
2974
+ else {
2975
+ cell = B.tmp();
2976
+ B.line(`${cell} = load ptr, ptr @scr_exc_active`);
2977
+ }
2978
+ word = B.tmp();
2979
+ B.line(`${word} = load i32, ptr ${cell}`);
2980
+ }
2981
+ const hit = B.tmp();
2982
+ const cold = B.tmp();
2983
+ B.line(`${hit} = icmp ne i32 ${word}, 0`);
2984
+ B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${hit}, i1 false)`);
2598
2985
  const lu = B.newLabel("exc.u");
2599
2986
  const lk = B.newLabel("exc.k");
2600
- B.condBr(p, lu, lk);
2987
+ if (this.mod.workers === true) {
2988
+ this.declare(`declare zeroext i1 @scr_exc_pending()`);
2989
+ const slow = B.newLabel("exc.s");
2990
+ B.condBr(cold, slow, lk);
2991
+ B.startBlock(slow);
2992
+ const p = B.tmp();
2993
+ B.line(`${p} = call zeroext i1 @scr_exc_pending()`);
2994
+ B.condBr(p, lu, lk);
2995
+ }
2996
+ else {
2997
+ B.condBr(cold, lu, lk);
2998
+ }
2601
2999
  B.startBlock(lu);
2602
- this.emitUnwind();
3000
+ this.emitUnwind(through);
2603
3001
  B.startBlock(lk);
2604
3002
  }
3003
+ /** After a runtime call that ALWAYS leaves an exception pending (the
3004
+ * compiler-resolved Node throws): the pending test would always answer
3005
+ * true, so unwind unconditionally. Emission continues in a block with no
3006
+ * predecessors so the caller's remaining instructions stay well-formed;
3007
+ * LLVM deletes it. */
3008
+ emitAlwaysPendingUnwind() {
3009
+ const B = this.B;
3010
+ if (B.isTerminated())
3011
+ return;
3012
+ this.emitUnwind();
3013
+ B.startBlock(B.newLabel("exc.dead"));
3014
+ }
3015
+ /** A recursive function's entry check: below the running stack's guard,
3016
+ * throw Node's RangeError and unwind like any other pending exception. */
3017
+ emitStackCheck() {
3018
+ const B = this.B;
3019
+ const tl = this.mod.workers === true ? "thread_local " : "";
3020
+ this.declare(`@scr_stack_guard = external ${tl}global ptr`);
3021
+ this.declare(`declare ptr @llvm.stacksave.p0()`);
3022
+ this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
3023
+ this.declare(`declare void @scr_stack_overflow()`);
3024
+ const sp = B.tmp();
3025
+ const guard = B.tmp();
3026
+ const low = B.tmp();
3027
+ const cold = B.tmp();
3028
+ B.line(`${sp} = call ptr @llvm.stacksave.p0()`);
3029
+ B.line(`${guard} = load ptr, ptr @scr_stack_guard`);
3030
+ B.line(`${low} = icmp ult ptr ${sp}, ${guard}`);
3031
+ B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${low}, i1 false)`);
3032
+ const over = B.newLabel("stack.over");
3033
+ const ok = B.newLabel("stack.ok");
3034
+ B.condBr(cold, over, ok);
3035
+ B.startBlock(over);
3036
+ B.line(`call void @scr_stack_overflow()`);
3037
+ this.emitUnwind();
3038
+ B.startBlock(ok);
3039
+ }
2605
3040
  workerLoopBudget() {
2606
3041
  if (!this.mod.workers)
2607
3042
  return null;
@@ -2699,6 +3134,11 @@ export class LlEmitter {
2699
3134
  const def = this.unionsById.get(v.type.unionId);
2700
3135
  if (!def)
2701
3136
  throw new InternalCompilerError(`llvm emitter bug: truthiness of unknown union ${v.type.unionId}`);
3137
+ const nullable = this.nullableUnions.get(def.id);
3138
+ // Object arms are always truthy: present iff neither unit encoding.
3139
+ if (nullable && isObjectArm(nullable))
3140
+ return emitNullablePresent(B, nullable, v.name);
3141
+ const unionId = def.id;
2702
3142
  const slot = B.slot();
2703
3143
  B.entryAllocas.push(`${slot} = alloca i1`);
2704
3144
  const join = B.newLabel("ut.j");
@@ -2711,10 +3151,10 @@ export class LlEmitter {
2711
3151
  valueName = this.unionGetBool(v.name);
2712
3152
  }
2713
3153
  else if (arm.kind === "string") {
2714
- valueName = this.unionPeek(v.name);
3154
+ valueName = this.unionPeek(v.name, unionId);
2715
3155
  }
2716
3156
  else if (arm.kind === "bigint" || arm.kind === "dyn" || arm.kind === "jsval") {
2717
- valueName = this.unionPeek(v.name);
3157
+ valueName = this.unionPeek(v.name, unionId);
2718
3158
  }
2719
3159
  const truthy = this.truthyOf(arm.kind, valueName, true);
2720
3160
  B.line(`store i1 ${truthy}, ptr ${slot}`);
@@ -2820,22 +3260,22 @@ export class LlEmitter {
2820
3260
  }
2821
3261
  }
2822
3262
  // ── 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;
3263
+ /** A union value's tag (i32): the box's tag word, or the NULL test of a
3264
+ * nullable union's pointer. */
3265
+ unionTag(uName, unionId) {
3266
+ return emitUnionTag(this.B, this.nullableUnions.get(unionId), uName);
3267
+ }
3268
+ /** Guard for helpers that build or read a union's `%ScrUnion` box
3269
+ * directly (library-shaped unions with fixed arms): a nullable union
3270
+ * reaching one is an emitter bug, never a silent miscompile. */
3271
+ requireBoxedUnion(unionId) {
3272
+ if (this.nullableUnions.has(unionId))
3273
+ throw new InternalCompilerError(`llvm emitter bug: nullable union ${unionId} reached a tagged-box helper`);
2830
3274
  }
2831
3275
  /** 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;
3276
+ * the runtime's is a static inline); a nullable union is its payload. */
3277
+ unionPeek(uName, unionId) {
3278
+ return emitUnionPeek(this.B, this.nullableUnions.get(unionId), uName);
2839
3279
  }
2840
3280
  /** Emits `switch` over a union's tag with one block per arm; each arm
2841
3281
  * body must TERMINATE its block (the callers branch to a join). The
@@ -2843,7 +3283,7 @@ export class LlEmitter {
2843
3283
  * reads can group equivalent storage prefixes and emit one representative. */
2844
3284
  unionTagSwitch(uName, def, arm, fieldGroups) {
2845
3285
  const B = this.B;
2846
- const tag = this.unionTag(uName);
3286
+ const tag = this.unionTag(uName, def.id);
2847
3287
  const bad = B.newLabel("u.bad");
2848
3288
  const labels = [];
2849
3289
  if (fieldGroups) {
@@ -2889,12 +3329,12 @@ export class LlEmitter {
2889
3329
  /** The +1 extraction of a union's single narrowed arm (unionNarrow /
2890
3330
  * the nullish-family reads): scalars via inline payload loads, ref arms
2891
3331
  * a retained peek. */
2892
- unionExtract(uName, arm) {
3332
+ unionExtract(uName, unionId, arm) {
2893
3333
  if (arm.kind === "f64" || arm.kind === "procStream")
2894
3334
  return this.unionGetF64(uName);
2895
3335
  if (arm.kind === "bool")
2896
3336
  return this.unionGetBool(uName);
2897
- return this.retainValue(this.unionPeek(uName), arm);
3337
+ return this.retainValue(this.unionPeek(uName, unionId), arm);
2898
3338
  }
2899
3339
  /** A scalar f64 arm's payload (scr_union_get_f64 inlined). Inline loads
2900
3340
  * keep stack boxes free of escaping calls, so LLVM can scalarize them. */
@@ -2918,8 +3358,15 @@ export class LlEmitter {
2918
3358
  /** Constructs a union box around an OWNED (+1, already moved) value —
2919
3359
  * the scr_union_new_* dispatch of unionWrap and the wrap-into-join
2920
3360
  * sites (shift). */
2921
- unionNewOwned(tag, v) {
3361
+ unionNewOwned(unionId, tag, v) {
2922
3362
  const B = this.B;
3363
+ const nullable = this.nullableUnions.get(unionId);
3364
+ if (nullable) {
3365
+ // The owned payload IS the nullable union's value.
3366
+ if (tag !== nullable.refTag)
3367
+ throw new InternalCompilerError(`llvm emitter bug: payload wrap into unit arm ${tag} of ${unionId}`);
3368
+ return v.name;
3369
+ }
2923
3370
  const t = B.tmp();
2924
3371
  if (v.type.kind === "f64" || v.type.kind === "procStream") {
2925
3372
  this.declare(`declare ptr @scr_union_new_f64(i32, double)`);
@@ -3062,6 +3509,42 @@ export class LlEmitter {
3062
3509
  B.line(`${z} = zext i1 ${value} to i8`);
3063
3510
  B.line(`store i8 ${z}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
3064
3511
  }
3512
+ /** Reads an int32-specialized class field: the i32 slot is the value,
3513
+ * and its widening is the exact double every program observation sees. */
3514
+ loadInt32Field(ptr, type) {
3515
+ const B = this.B;
3516
+ const raw = B.tmp();
3517
+ const number = B.tmp();
3518
+ B.line(`${raw} = load i32, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
3519
+ B.line(`${number} = sitofp i32 ${raw} to double`);
3520
+ return {
3521
+ name: number,
3522
+ type,
3523
+ uint32: raw,
3524
+ integer: { name: raw, type: "i32", signed: true, range: INT32_RANGE },
3525
+ };
3526
+ }
3527
+ /** Stores a value the whole-program analysis proved to be an int32 (never
3528
+ * -0, NaN or fractional) into an int32-specialized class field. */
3529
+ storeInt32Field(ptr, value, expr) {
3530
+ if (!this.int32Slots.provenWrites.has(expr))
3531
+ throw new InternalCompilerError("llvm emitter bug: unproven int32 field write");
3532
+ const B = this.B;
3533
+ let bits;
3534
+ if (value.integer?.type === "i32")
3535
+ bits = value.integer.name;
3536
+ else if (value.integer) {
3537
+ bits = B.tmp();
3538
+ B.line(`${bits} = trunc i64 ${value.integer.name} to i32`);
3539
+ }
3540
+ else if (value.uint32 !== undefined)
3541
+ bits = value.uint32;
3542
+ else {
3543
+ bits = B.tmp();
3544
+ B.line(`${bits} = fptosi double ${value.name} to i32`);
3545
+ }
3546
+ B.line(`store i32 ${bits}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
3547
+ }
3065
3548
  // ── bindings ────────────────────────────────────────────────────────────
3066
3549
  /** A binding's storage: a module global, a plain local slot, or a boxed
3067
3550
  * local (the slot holds the capture BOX; access goes through it). */
@@ -3375,6 +3858,9 @@ export class LlEmitter {
3375
3858
  emitFunction(fn) {
3376
3859
  const B = new BlockBuilder();
3377
3860
  this.B = B;
3861
+ // Fiber switches restore the active exception cell before control
3862
+ // returns to a synchronous frame (emitPendingCheck).
3863
+ B.excCellInvariant = fn.async !== true && fn.generator === undefined;
3378
3864
  this.debugScope = this.debug?.function(fn) ?? null;
3379
3865
  B.debugLocation = this.debug?.location(fn.loc, this.debugScope) ?? null;
3380
3866
  this.frames = [];
@@ -3407,6 +3893,7 @@ export class LlEmitter {
3407
3893
  const numericFn = withInitializerBindings(fn, initializerBindings);
3408
3894
  this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
3409
3895
  this.borrowedParameters.clear();
3896
+ this.reboundOwnerSlots.clear();
3410
3897
  this.projectedParameters.clear();
3411
3898
  const projectedParameterIndexes = this.callLifetimes.parameters.get(fn.name);
3412
3899
  if (projectedParameterIndexes)
@@ -3415,8 +3902,12 @@ export class LlEmitter {
3415
3902
  this.stableCallBindings = this.callLifetimes.bindings.get(fn.name) ?? new Set();
3416
3903
  const borrowedParameterIndexes = this.callLifetimes.borrowed.get(fn.name);
3417
3904
  if (borrowedParameterIndexes) {
3905
+ // Rebound walk parameters borrow too, but only unchanged ones may
3906
+ // serve as stable owners for call arguments and aliases.
3418
3907
  for (const index of borrowedParameterIndexes)
3419
- this.borrowedParameters.add(fn.params[index].localId);
3908
+ if (!this.walkBorrowsByFunction.get(fn.name)?.has(fn.params[index].localId) &&
3909
+ !this.reboundByFunction.get(fn.name)?.has(fn.params[index].localId))
3910
+ this.borrowedParameters.add(fn.params[index].localId);
3420
3911
  }
3421
3912
  this.captureIds = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId));
3422
3913
  this.integerLoopBindings.clear();
@@ -3437,17 +3928,19 @@ export class LlEmitter {
3437
3928
  this.callArrayReads = findCallArrayReads(fn, this.optionalArrayReads, this.referenceEffects.functions, this.callLifetimes);
3438
3929
  this.scalarStringSlices =
3439
3930
  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);
3931
+ this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
3932
+ this.localStackUnions = findLocalStackUnions(fn, this.callLifetimes, this.boxedUnionsById);
3933
+ this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.boxedUnionsById, (e) => this.nullableFieldGet(e) !== null);
3443
3934
  this.localUnionStorage = new Map();
3444
- this.integerRanges = analyzeIntegerRanges(numericFn);
3935
+ this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set();
3936
+ this.integerRanges = analyzeIntegerRanges(numericFn, this.int32Slots.facts(fn.name));
3445
3937
  this.bytesBounds = findBytesBounds(numericFn, this.integerRanges);
3446
3938
  this.chainSlots.clear();
3447
3939
  this.finallyStack = [];
3448
3940
  this.tryStack = [];
3449
3941
  this.currentReturnType = fn.returnType;
3450
3942
  this.currentGenerator = fn.generator ?? null;
3943
+ this.currentBorrowedReturn = this.borrowedReturns.has(fn.name);
3451
3944
  this.currentWasiCoro = null;
3452
3945
  this.logArgSlots = 0;
3453
3946
  if (this.wasi && (fn.async === true || fn.generator !== undefined)) {
@@ -3551,7 +4044,12 @@ export class LlEmitter {
3551
4044
  const cache = B.slot();
3552
4045
  B.entryAllocas.push(`${cache} = alloca ptr ; lazy box ${this.currentLocals.get(id).name}`);
3553
4046
  B.line(`store ptr null, ptr ${cache}`);
3554
- fnScope.push({ slot: cache, type: this.currentLocals.get(id).type, boxed: true });
4047
+ fnScope.push({
4048
+ slot: cache,
4049
+ type: this.currentLocals.get(id).type,
4050
+ boxed: true,
4051
+ lazy: true,
4052
+ });
3555
4053
  this.lazyCaptureBoxes.set(id, cache);
3556
4054
  // Environments that finish during the call can share an immortal
3557
4055
  // frame box. Its payload owner is released with the function scope.
@@ -3578,6 +4076,15 @@ export class LlEmitter {
3578
4076
  this.storeIntegerView(p.localId, { name: `%p_${mangleLocal(p.localId)}`, type: p.type });
3579
4077
  if (isRefCounted(p.type) && !borrowed?.has(index))
3580
4078
  fnScope.push({ slot, type: p.type });
4079
+ if (this.reboundByFunction.get(fn.name)?.has(p.localId) && borrowed?.has(index)) {
4080
+ // The caller's borrow stays in the parameter slot; values the body
4081
+ // assigns are owned here until rebinding or exit releases them.
4082
+ const owner = `${slot}.owner`;
4083
+ B.entryAllocas.push(`${owner} = alloca ptr`);
4084
+ B.line(`store ptr null, ptr ${owner}`);
4085
+ fnScope.push({ slot: owner, type: p.type });
4086
+ this.reboundOwnerSlots.set(p.localId, owner);
4087
+ }
3581
4088
  }
3582
4089
  this.scopes.push(fnScope);
3583
4090
  // Stackful native fibers retain these frames while suspended. The
@@ -3590,7 +4097,9 @@ export class LlEmitter {
3590
4097
  B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${this.cstr(frame)})`);
3591
4098
  B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
3592
4099
  }
3593
- if (this.mod.workers)
4100
+ if (this.stackChecks?.has(fn.name))
4101
+ this.emitStackCheck();
4102
+ if (this.workerEntryPolls?.has(fn.name))
3594
4103
  this.emitPendingCheck();
3595
4104
  this.emitStmts(fn.body);
3596
4105
  // Implicit exit of a void function: release the function scope unless
@@ -3662,24 +4171,55 @@ export class LlEmitter {
3662
4171
  const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`;
3663
4172
  const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
3664
4173
  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;
4174
+ if (!borrowed)
4175
+ return body;
4176
+ const slot = this.implSlotBorrowed(fn.name);
4177
+ const virtual = slot.size > 0 && slot.size !== borrowed.size
4178
+ ? "\n" + this.emitOwnedCallAdapter(fn, borrowed, slot)
4179
+ : "";
4180
+ return body + "\n" + this.emitOwnedCallAdapter(fn, borrowed) + virtual;
3666
4181
  }
3667
4182
  /** Keep the ordinary owned ABI for closures, virtual dispatch, generated
3668
4183
  * runtime adapters and library exports. Only direct IR calls select the
3669
4184
  * borrowing body. Ownership of other parameters moves into that body;
3670
4185
  * borrowed parameters are released here on normal and exceptional exits.
3671
4186
  * The pending exception remains for the adapter's caller to handle. */
3672
- emitOwnedCallAdapter(fn, borrowed) {
4187
+ /** With `kept`, the virtual adapter of a borrowing slot instead: the
4188
+ * slot's parameters stay borrowed, the body's other borrowed ones are
4189
+ * released here. */
4190
+ emitOwnedCallAdapter(fn, borrowed, kept = NO_BORROWED) {
3673
4191
  const B = new BlockBuilder();
3674
4192
  this.B = B;
3675
4193
  const params = fn.params.map((param, index) => `${this.llType(param.type)} %p${index}`);
3676
4194
  const ret = this.llType(fn.returnType);
3677
4195
  const call = `call ${ret} @${mangleBorrowedFunction(fn.name)}(${params.join(", ")})`;
3678
4196
  B.line(ret === "void" ? call : `%result = ${call}`);
4197
+ // A borrowed result is reachable from the parameters: own it first. A
4198
+ // throwing body returns a null dummy, which needs no owner (and string
4199
+ // or array retains do not accept NULL).
4200
+ let result = "%result";
4201
+ if (this.borrowedReturns.has(fn.name)) {
4202
+ const slot = B.slot();
4203
+ B.entryAllocas.push(`${slot} = alloca ptr`);
4204
+ B.line(`store ptr null, ptr ${slot}`);
4205
+ const present = B.tmp();
4206
+ const own = B.newLabel("result.own");
4207
+ const done = B.newLabel("result.done");
4208
+ B.line(`${present} = icmp ne ptr %result, null`);
4209
+ B.condBr(present, own, done);
4210
+ B.startBlock(own);
4211
+ B.line(`store ptr ${this.retainValue("%result", fn.returnType)}, ptr ${slot}`);
4212
+ B.br(done);
4213
+ B.startBlock(done);
4214
+ result = B.tmp();
4215
+ B.line(`${result} = load ptr, ptr ${slot}`);
4216
+ }
3679
4217
  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}`;
4218
+ if (!kept.has(index))
4219
+ this.releaseValue(`%p${index}`, fn.params[index].type);
4220
+ B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`);
4221
+ const symbol = kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name);
4222
+ return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`;
3683
4223
  }
3684
4224
  /** A local stable binding keeps its value alive while later arguments
3685
4225
  * and the callee execute. A proven local union box cannot escape or be
@@ -3688,6 +4228,33 @@ export class LlEmitter {
3688
4228
  canBorrowCallArgument(value) {
3689
4229
  if (value.kind === "strLit")
3690
4230
  return true;
4231
+ // Class casts reinterpret the same pointer (prefix layouts).
4232
+ if ((value.kind === "upcast" || value.kind === "downcast") &&
4233
+ value.value.type.kind === "object")
4234
+ return this.canBorrowCallArgument(value.value);
4235
+ if (value.kind === "seqExpr") {
4236
+ const guarded = guardedValue(value);
4237
+ return guarded !== null && this.canBorrowCallArgument(guarded);
4238
+ }
4239
+ // A nullable-union wrap reinterprets its payload pointer; an immortal
4240
+ // unit constant needs no owner at all.
4241
+ const wrap = this.borrowableNullableWrap(value);
4242
+ if (wrap === "unit")
4243
+ return true;
4244
+ if (wrap === "ref" && value.kind === "unionWrap")
4245
+ return this.canBorrowCallArgument(value.value);
4246
+ // Checked casts (`x as C`, `x!` on a nullable) lower to a ternary whose
4247
+ // failing arm always throws; the successful arm projects the same
4248
+ // stable owner. The throwing arm's typed dummy owns nothing.
4249
+ if (value.kind === "unionNarrow")
4250
+ return this.canBorrowCallArgument(value.value);
4251
+ if (value.kind === "libCall")
4252
+ return value.fn === "error.nodeThrow";
4253
+ if (value.kind === "ternary")
4254
+ return (this.canBorrowReceiver(value.then) &&
4255
+ this.canBorrowReceiver(value.else_) &&
4256
+ this.canBorrowCallArgument(value.then) &&
4257
+ this.canBorrowCallArgument(value.else_));
3691
4258
  if (value.kind !== "varRef")
3692
4259
  return false;
3693
4260
  const binding = this.binding(value.localId);
@@ -3742,6 +4309,12 @@ export class LlEmitter {
3742
4309
  switch (s.kind) {
3743
4310
  case "varDecl": {
3744
4311
  const b = this.binding(s.localId);
4312
+ if (this.walkBorrows.has(s.localId) && b.kind === "local") {
4313
+ // A borrowed walk pointer: no retain now, no release at scope exit.
4314
+ const v = s.init === null ? "null" : this.emitReadReceiver(s.init).name;
4315
+ B.line(`store ptr ${v}, ptr ${b.slot}`);
4316
+ break;
4317
+ }
3745
4318
  const slice = this.scalarStringSlices.get(s.localId);
3746
4319
  if (slice) {
3747
4320
  const snapshot = emitStringSliceSnapshot(this, slice);
@@ -3874,6 +4447,23 @@ export class LlEmitter {
3874
4447
  break;
3875
4448
  }
3876
4449
  case "assign": {
4450
+ const owner = this.reboundOwnerSlots.get(s.localId);
4451
+ if (owner !== undefined && !this.walkBorrows.has(s.localId)) {
4452
+ // A rebound borrowed parameter: the new value moves into the owner
4453
+ // slot, replacing (and releasing) the previous owned value. This
4454
+ // precedes the in-place string append, which would release the
4455
+ // caller's borrow.
4456
+ const param = this.binding(s.localId);
4457
+ const slot = param.slot;
4458
+ const v = this.emitExpr(s.value);
4459
+ this.moveTemp(v);
4460
+ const old = B.tmp();
4461
+ B.line(`${old} = load ptr, ptr ${owner}`);
4462
+ this.releaseValue(old, param.type);
4463
+ B.line(`store ptr ${v.name}, ptr ${slot}`);
4464
+ B.line(`store ptr ${v.name}, ptr ${owner}`);
4465
+ break;
4466
+ }
3877
4467
  const localStorage = this.localUnionStorage.get(s.localId);
3878
4468
  if (localStorage) {
3879
4469
  storeLocalUnion(this, localStorage, s.value);
@@ -3886,6 +4476,11 @@ export class LlEmitter {
3886
4476
  break;
3887
4477
  }
3888
4478
  const b = this.binding(s.localId);
4479
+ if (this.walkBorrows.has(s.localId) && b.kind === "local") {
4480
+ // Rebinding a borrowed walk pointer releases nothing.
4481
+ B.line(`store ptr ${this.emitReadReceiver(s.value).name}, ptr ${b.slot}`);
4482
+ break;
4483
+ }
3889
4484
  const v = this.emitExpr(s.value);
3890
4485
  if (b.kind === "global" && !s.initializes)
3891
4486
  this.checkGlobalTdz(s.localId);
@@ -3907,7 +4502,9 @@ export class LlEmitter {
3907
4502
  break;
3908
4503
  }
3909
4504
  case "exprStmt":
3910
- this.emitExpr(s.expr);
4505
+ // A statement-position splice never observes its removed elements.
4506
+ if (!emitDiscardedSplice(this, s.expr))
4507
+ this.emitDiscarded(s.expr);
3911
4508
  break;
3912
4509
  case "arraySet": {
3913
4510
  // Evaluation order matches JS: array, index, then value. Ownership
@@ -3919,19 +4516,43 @@ export class LlEmitter {
3919
4516
  if (s.arr.type.kind !== "array")
3920
4517
  throw new InternalCompilerError("llvm emitter bug: arraySet on non-array");
3921
4518
  const acc = elemAccess(s.arr.type.elem);
4519
+ this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
3922
4520
  if (acc === "ref")
3923
4521
  this.moveTemp(v);
3924
4522
  emitDenseArraySet(this, arr.name, idx, s.index, acc, s.arr.type.elem, v.name);
3925
4523
  break;
3926
4524
  }
3927
4525
  case "arraySetLength": {
4526
+ // Pop's `length = 0` store on an empty array only matters when the
4527
+ // array can be frozen (published); its operand is a local.
4528
+ if (s.pop === true &&
4529
+ s.length.kind === "numLit" &&
4530
+ s.length.value === 0 &&
4531
+ this.publishedTypes?.containerGuarded(s.arr.type) !== true)
4532
+ break;
3928
4533
  const arr = emitBorrowedInput(this, s.arr);
3929
4534
  const length = this.emitExpr(s.length);
3930
4535
  if (s.arr.type.kind !== "array")
3931
4536
  throw new InternalCompilerError("llvm emitter bug: arraySetLength on non-array");
4537
+ this.emitPublishedArrayGuard(arr.name, s.arr.type, s.pop === true ? 2 : 3);
3932
4538
  this.declare(`declare void @scr_arr_set_len(ptr, double)`);
4539
+ // Assigning the current length (a reused work array reset to where
4540
+ // it already is) changes nothing: the runtime's validation accepts
4541
+ // exactly the value equal to the length (-0 included, NaN never),
4542
+ // and no element is released. Everything else keeps the call.
4543
+ const lenPtr = B.tmp(), len = B.tmp(), lenNumber = B.tmp(), same = B.tmp();
4544
+ const resize = B.newLabel("arr.len.set"), done = B.newLabel("arr.len.done");
4545
+ B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
4546
+ this.markMemoryPointer(lenPtr, "array:header");
4547
+ B.line(`${len} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`);
4548
+ B.line(`${lenNumber} = uitofp ${this.sizeType} ${len} to double`);
4549
+ B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`);
4550
+ B.condBr(same, done, resize);
4551
+ B.startBlock(resize);
3933
4552
  B.line(`call void @scr_arr_set_len(ptr ${arr.name}, double ${length.name})`);
3934
4553
  this.emitPendingCheck();
4554
+ B.br(done);
4555
+ B.startBlock(done);
3935
4556
  break;
3936
4557
  }
3937
4558
  case "arraySetUndefined":
@@ -3940,6 +4561,7 @@ export class LlEmitter {
3940
4561
  const idx = this.emitExpr(s.index);
3941
4562
  if (s.arr.type.kind !== "array")
3942
4563
  throw new InternalCompilerError(`llvm emitter bug: ${s.kind} on non-array`);
4564
+ this.emitPublishedArrayGuard(arr.name, s.arr.type, s.kind === "arrayDelete" ? 5 : 0, idx.name);
3943
4565
  const fn = s.kind === "arraySetUndefined" ? "scr_arr_set_undefined" : "scr_arr_delete";
3944
4566
  this.declare(`declare ${s.kind === "arrayDelete" ? "zeroext i1" : "void"} @${fn}(ptr, double)`);
3945
4567
  if (s.kind === "arrayDelete")
@@ -3971,14 +4593,27 @@ export class LlEmitter {
3971
4593
  // Classes and records share the struct layout, so one emission.
3972
4594
  const nullable = s.kind === "fieldSet" ? this.nullableFields.get(s.className, s.field) : null;
3973
4595
  if (nullable && s.kind === "fieldSet") {
3974
- const obj = this.emitExpr(s.obj);
4596
+ const obj = this.emitStableReceiver(s.obj, [s.value]);
4597
+ // (The guard precedes the value here: the nullable store
4598
+ // evaluates it itself.)
4599
+ this.emitPublishedFieldGuard(obj.name, s.className, s.field);
3975
4600
  this.emitNullableFieldStore(obj.name, s.className, s.field, nullable, s.value);
3976
4601
  break;
3977
4602
  }
3978
- const obj = isRefCounted(s.value.type)
3979
- ? this.emitExpr(s.obj)
3980
- : this.emitStableReceiver(s.obj, [s.value]);
4603
+ if (s.kind === "fieldSet" && this.int32Slots.isField(s.className, s.field)) {
4604
+ const obj = this.emitStableReceiver(s.obj, [s.value]);
4605
+ const v = this.emitExpr(s.value);
4606
+ this.emitPublishedFieldGuard(obj.name, s.className, s.field);
4607
+ const { ptr } = this.classFieldPtr(obj.name, s.className, s.field);
4608
+ this.storeInt32Field(ptr, v, s.value);
4609
+ break;
4610
+ }
4611
+ const obj = this.emitStableReceiver(s.obj, [s.value]);
3981
4612
  const v = this.emitExpr(s.value);
4613
+ if (s.kind === "fieldSet")
4614
+ this.emitPublishedFieldGuard(obj.name, s.className, s.field);
4615
+ else if (this.publishedTypes?.recordGuarded(s.shapeId) === true)
4616
+ this.emitPublishedFieldGuard(obj.name, null, s.field);
3982
4617
  const { ptr, type } = s.kind === "fieldSet"
3983
4618
  ? this.classFieldPtr(obj.name, s.className, s.field)
3984
4619
  : this.recordFieldPtr(obj.name, s.shapeId, s.field);
@@ -4012,6 +4647,11 @@ export class LlEmitter {
4012
4647
  // the overflow (key and value released; absent keys no-op).
4013
4648
  const obj = this.emitExpr(s.obj);
4014
4649
  const key = this.emitExpr(s.key);
4650
+ if (this.publishedTypes?.recordGuarded(s.shapeId) === true) {
4651
+ // Node: "Cannot delete property 'k' of #<Object>" (key known at
4652
+ // run time only; the guard reports the frozen record generically).
4653
+ this.emitPublishedFieldGuard(obj.name, null, "[key]");
4654
+ }
4015
4655
  const ovf = this.recordOvfPtr(obj.name, s.shapeId);
4016
4656
  this.declare(`declare zeroext i1 @scr_map_delete_str(ptr, ptr)`);
4017
4657
  const t = B.tmp();
@@ -4027,6 +4667,8 @@ export class LlEmitter {
4027
4667
  const obj = this.emitExpr(s.obj);
4028
4668
  const key = this.emitExpr(s.key);
4029
4669
  const v = this.emitExpr(s.value);
4670
+ if (this.publishedTypes?.recordGuarded(s.shapeId) === true)
4671
+ this.emitPublishedFieldGuard(obj.name, null, "[key]");
4030
4672
  if (isRefCounted(v.type))
4031
4673
  this.moveTemp(v);
4032
4674
  const shape = this.recordShape(s.shapeId);
@@ -4081,9 +4723,9 @@ export class LlEmitter {
4081
4723
  B.line(`call void @scr_dyn_release(ptr ${v.name})`);
4082
4724
  // Mismatched write: TypeError pending, field untouched — the
4083
4725
  // statement-level check below unwinds.
4084
- this.declare(`declare zeroext i1 @scr_exc_pending()`);
4085
4726
  const pend = B.tmp();
4086
- B.line(`${pend} = call zeroext i1 @scr_exc_pending()`);
4727
+ for (const line of this.pendingTestLines(pend))
4728
+ B.line(line.trimStart());
4087
4729
  const lw = B.newLabel("rks.w");
4088
4730
  B.condBr(pend, join, lw);
4089
4731
  B.startBlock(lw);
@@ -4611,7 +5253,12 @@ export class LlEmitter {
4611
5253
  // the actual ret. The parked value owns a synthetic slot-backed
4612
5254
  // scope entry during each copy so a throwing finally releases it.
4613
5255
  let v = null;
4614
- if (s.value !== null) {
5256
+ if (s.value !== null && this.currentBorrowedReturn) {
5257
+ // A borrowed-return body hands back its walk without a reference
5258
+ // (it has no try statements, so no finally can intervene).
5259
+ v = this.emitReadReceiver(s.value);
5260
+ }
5261
+ else if (s.value !== null) {
4615
5262
  v = this.emitExpr(s.value);
4616
5263
  this.moveTemp(v);
4617
5264
  }
@@ -4986,24 +5633,62 @@ export class LlEmitter {
4986
5633
  return emitLiteralExpr(this, e);
4987
5634
  }
4988
5635
  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
- }
5636
+ if (e.kind === "seqExpr")
5637
+ return this.emitSequence(() => emitBorrowedFieldSequence(this, e) ?? emitOperatorExpr(this, e));
5005
5638
  return emitOperatorExpr(this, e);
5006
5639
  }
5640
+ /** Saved operands can feed later call arguments. Transfer ownership of a
5641
+ * sequence's locals to this expression's frame so they survive those
5642
+ * reads and still die on the path that created them, including inside
5643
+ * lazy branches. */
5644
+ emitSequence(body) {
5645
+ this.scopes.push([]);
5646
+ const result = body();
5647
+ for (const local of this.scopes.pop()) {
5648
+ this.currentFrame().push({
5649
+ name: local.slot,
5650
+ type: local.type,
5651
+ slot: true,
5652
+ ...(local.boxed ? { boxed: true } : {}),
5653
+ });
5654
+ }
5655
+ return result;
5656
+ }
5657
+ /** A statement-position expression whose value is dropped. When a
5658
+ * sequence's result only reads an unchanged local (a derived
5659
+ * constructor's `super()` evaluates to `this`), its statements still run
5660
+ * in place, but the result is neither retained nor released. */
5661
+ /** Whether a discarded value can be skipped entirely: a borrowable read
5662
+ * with no effect of its own. canBorrowCallArgument also accepts checked
5663
+ * projections (a ternary or narrow whose failing arm throws, and the
5664
+ * throwing call itself) because a borrowed use still evaluates them;
5665
+ * skipping those would drop the throw. */
5666
+ discardableRead(value) {
5667
+ if (value.kind === "libCall" || value.kind === "ternary" || value.kind === "unionNarrow")
5668
+ return false;
5669
+ if ((value.kind === "upcast" || value.kind === "downcast" || value.kind === "unionWrap") &&
5670
+ !this.discardableRead(value.value))
5671
+ return false;
5672
+ if (value.kind === "seqExpr") {
5673
+ const guarded = guardedValue(value);
5674
+ if (guarded === null || !this.discardableRead(guarded))
5675
+ return false;
5676
+ }
5677
+ return this.canBorrowCallArgument(value);
5678
+ }
5679
+ emitDiscarded(e) {
5680
+ if (e.kind === "seqExpr" &&
5681
+ isRefCounted(e.result.type) &&
5682
+ e.result.kind !== "strLit" &&
5683
+ this.discardableRead(e.result)) {
5684
+ this.emitSequence(() => {
5685
+ for (const s of e.stmts)
5686
+ this.emitStmt(s);
5687
+ });
5688
+ return;
5689
+ }
5690
+ this.emitExpr(e);
5691
+ }
5007
5692
  emitControlExpr(e) {
5008
5693
  return emitControlExpr(this, e);
5009
5694
  }
@@ -5017,8 +5702,17 @@ export class LlEmitter {
5017
5702
  * value, making the snapshot unique unless a real observable alias exists.
5018
5703
  */
5019
5704
  emitStringSelfConcatAssign(localId, left, suffix, retainForYield) {
5705
+ const moved = this.emitMovedSelfConcat(localId, left, suffix, retainForYield);
5706
+ if (moved)
5707
+ return moved;
5020
5708
  const snapshot = this.emitExpr(left);
5021
- const right = this.emitExpr(suffix);
5709
+ // A suffix of several parts, or a number, appends each operand to the
5710
+ // snapshot directly (in place once the binding lets go of it) instead
5711
+ // of first building the suffix as a separate string.
5712
+ const parts = stringParts(suffix);
5713
+ const mixed = parts.length > 1 || numberPart(parts[0]) !== null;
5714
+ const inputs = mixed ? emitConcatInputs(this, parts) : [];
5715
+ const right = mixed ? null : this.emitExpr(suffix);
5022
5716
  const b = this.binding(localId);
5023
5717
  const B = this.B;
5024
5718
  if (b.kind === "boxed") {
@@ -5031,9 +5725,15 @@ export class LlEmitter {
5031
5725
  B.line(`store ptr null, ptr ${b.slot}`);
5032
5726
  this.releaseValue(old, b.type);
5033
5727
  }
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})`);
5728
+ let raw;
5729
+ if (right === null) {
5730
+ raw = emitMixedConcat(this, snapshot.name, inputs);
5731
+ }
5732
+ else {
5733
+ this.declare(`declare ptr @scr_str_concat(ptr, ptr)`);
5734
+ raw = B.tmp();
5735
+ B.line(`${raw} = call ptr @scr_str_concat(ptr ${snapshot.name}, ptr ${right.name})`);
5736
+ }
5037
5737
  const result = this.own({ name: raw, type: left.type });
5038
5738
  if (retainForYield) {
5039
5739
  const stored = this.retainValue(result.name, result.type);
@@ -5051,6 +5751,43 @@ export class LlEmitter {
5051
5751
  }
5052
5752
  return result;
5053
5753
  }
5754
+ /** `local = local + suffix` when the suffix cannot read or write the
5755
+ * local (an unboxed binding is only reachable through this function's own
5756
+ * expressions): reading the binding after the suffix gives the value the
5757
+ * left side had, so it moves into a consuming concat, and the result takes
5758
+ * its place. Null when the general snapshot path is needed. */
5759
+ emitMovedSelfConcat(localId, left, suffix, retainForYield) {
5760
+ const b = this.binding(localId);
5761
+ if (retainForYield ||
5762
+ b.kind !== "local" ||
5763
+ b.local === undefined ||
5764
+ b.local.boxed ||
5765
+ b.local.tdz ||
5766
+ b.type.kind !== "string" ||
5767
+ left.kind !== "varRef" ||
5768
+ left.localId !== localId ||
5769
+ mentionsLocal(suffix, localId))
5770
+ return null;
5771
+ const parts = stringParts(suffix);
5772
+ const mixed = parts.length > 1 || numberPart(parts[0]) !== null;
5773
+ const inputs = mixed ? emitConcatInputs(this, parts) : [];
5774
+ const right = mixed ? null : this.emitExpr(suffix);
5775
+ this.materializeSplitLocal(localId);
5776
+ const B = this.B;
5777
+ const head = B.tmp();
5778
+ B.line(`${head} = load ptr, ptr ${b.slot}`);
5779
+ let raw;
5780
+ if (right === null) {
5781
+ raw = emitMixedConcat(this, head, inputs, true);
5782
+ }
5783
+ else {
5784
+ this.declare(`declare ptr @scr_str_concat_move(ptr, ptr)`);
5785
+ raw = B.tmp();
5786
+ B.line(`${raw} = call ptr @scr_str_concat_move(ptr ${head}, ptr ${right.name})`);
5787
+ }
5788
+ B.line(`store ptr ${raw}, ptr ${b.slot}`);
5789
+ return { name: raw, type: left.type }; // moved into the binding
5790
+ }
5054
5791
  emitContainerExpr(e) {
5055
5792
  return emitContainerExpr(this, e);
5056
5793
  }
@@ -5064,8 +5801,152 @@ export class LlEmitter {
5064
5801
  return emitDynamicExpr(this, e);
5065
5802
  }
5066
5803
  emitIntrinsicExpr(e) {
5804
+ if (e.name === "threads.publish")
5805
+ return this.emitPublish(e);
5067
5806
  return emitIntrinsicExpr(this, e);
5068
5807
  }
5808
+ publishWalkersCache = null;
5809
+ /** Per-type publication walkers, created on the first publish. */
5810
+ get publishWalkers() {
5811
+ if (this.publishWalkersCache !== null)
5812
+ return this.publishWalkersCache;
5813
+ const byPre = [...this.classMeta.values()].sort((a, b) => a.pre - b.pre);
5814
+ this.publishWalkersCache = new PublishWalkers({
5815
+ declare: (decl) => this.declare(decl),
5816
+ cstr: (text) => this.cstr(text),
5817
+ sizeType: this.sizeType,
5818
+ tracedShapes: this.tracedShapes,
5819
+ unionsById: this.unionsById,
5820
+ recordsById: this.recordsById,
5821
+ nullableArm: (t) => this.nullableUnions.of(t)?.arm ?? null,
5822
+ classInfo: (className) => {
5823
+ const meta = this.classMetaOf(className);
5824
+ return {
5825
+ def: meta.def,
5826
+ hierarchy: meta.hierarchy,
5827
+ pre: meta.pre,
5828
+ post: meta.post,
5829
+ runtimeRooted: meta.root.def.runtime === true,
5830
+ fields: meta.def.fields.map((f) => ({
5831
+ name: f.name,
5832
+ index: classFieldIndex(meta, f.name).index,
5833
+ type: this.nullableFields.storageType(className, f.name, f.type),
5834
+ })),
5835
+ };
5836
+ },
5837
+ classesInInterval: (pre, post) => byPre.filter((m) => m.pre >= pre && m.pre <= post).map((m) => m.def.name),
5838
+ });
5839
+ return this.publishWalkersCache;
5840
+ }
5841
+ /** `publish(value)` (@scriptc/threads): makes the graph immortal and
5842
+ * immutable (scr_publish with the static type's walker) and answers the
5843
+ * same value. Throws (pending) when a dyn part is unpublishable. */
5844
+ emitPublish(e) {
5845
+ const value = this.emitExpr(e.args[0]);
5846
+ if (this.B.isTerminated())
5847
+ return value;
5848
+ const lines = this.publishWalkers.emitPublish(value.name, value.type, e.loc);
5849
+ if (lines.length > 0) {
5850
+ for (const line of lines)
5851
+ this.B.line(line.trimStart());
5852
+ // A refusal unwinds while the operand temp still owns its reference.
5853
+ this.emitPendingCheck();
5854
+ }
5855
+ // The result IS the operand: ownership moves to the result temp.
5856
+ if (!isRefCounted(value.type))
5857
+ return { name: value.name, type: e.type };
5858
+ this.moveTemp(value);
5859
+ return this.own({ name: value.name, type: e.type });
5860
+ }
5861
+ /** Branches on "`obj` is published" (rc == SIZE_MAX); leaves the builder
5862
+ * in the cold block and answers the continuation label. */
5863
+ emitPublishedTest(obj) {
5864
+ const B = this.B;
5865
+ const rc = B.tmp(), frozen = B.tmp(), expected = B.tmp();
5866
+ B.line(`${rc} = load ${this.sizeType}, ptr ${obj}`);
5867
+ B.line(`${frozen} = icmp eq ${this.sizeType} ${rc}, -1`);
5868
+ this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
5869
+ B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${frozen}, i1 false)`);
5870
+ const cold = B.newLabel("pub.frozen"), ok = B.newLabel("pub.ok");
5871
+ B.condBr(expected, cold, ok);
5872
+ B.startBlock(cold);
5873
+ return ok;
5874
+ }
5875
+ publishedNamesSym = null;
5876
+ /** The class-name table the hierarchy field guards index by preorder. */
5877
+ publishedClassNamesDefs() {
5878
+ if (this.publishedNamesSym === null)
5879
+ return [];
5880
+ const metas = [...this.classMeta.values()];
5881
+ const size = metas.reduce((max, m) => Math.max(max, m.pre + 1), 0);
5882
+ const names = new Array(size).fill("ptr null");
5883
+ for (const m of metas)
5884
+ names[m.pre] = `ptr ${this.cstr(m.def.jsName ?? m.def.name)}`;
5885
+ return [
5886
+ `${this.publishedNamesSym} = internal constant [${size} x ptr] [ ${names.join(", ")} ]`,
5887
+ ``,
5888
+ ];
5889
+ }
5890
+ /** The frozen-object guard of a class field or record field store:
5891
+ * Node's "Cannot assign to read only property" TypeError when the target
5892
+ * is published. `className` null means a record (`#<Object>`). */
5893
+ emitPublishedFieldGuard(obj, className, field) {
5894
+ const published = this.publishedTypes;
5895
+ if (published === null)
5896
+ return;
5897
+ if (className !== null && !published.classGuarded(className))
5898
+ return;
5899
+ const B = this.B;
5900
+ const ok = this.emitPublishedTest(obj);
5901
+ let owner;
5902
+ const meta = className === null ? undefined : this.classMeta.get(className);
5903
+ if (meta === undefined)
5904
+ owner = this.cstr("Object");
5905
+ else if (!meta.hierarchy)
5906
+ owner = this.cstr(meta.def.jsName ?? meta.def.name);
5907
+ else {
5908
+ // The DYNAMIC class names the object, as in Node.
5909
+ this.publishedNamesSym ??= "@sc_pub_class_names";
5910
+ const vtp = B.tmp(), vt = B.tmp(), pre = B.tmp(), slot = B.tmp(), name = B.tmp();
5911
+ B.line(`${vtp} = getelementptr inbounds ptr, ptr ${obj}, i64 1`);
5912
+ B.line(`${vt} = load ptr, ptr ${vtp}`);
5913
+ B.line(`${pre} = load ${this.sizeType}, ptr ${vt}`);
5914
+ B.line(`${slot} = getelementptr inbounds ptr, ptr ${this.publishedNamesSym}, ${this.sizeType} ${pre}`);
5915
+ B.line(`${name} = load ptr, ptr ${slot}`);
5916
+ owner = name;
5917
+ }
5918
+ this.declare(`declare void @scr_throw_published_field(ptr, ptr)`);
5919
+ B.line(`call void @scr_throw_published_field(ptr ${this.cstr(field)}, ptr ${owner})`);
5920
+ this.emitUnwind();
5921
+ B.startBlock(ok);
5922
+ }
5923
+ /** The frozen-array guard (scr_throw_published_array's ops): `index` and
5924
+ * `count` are doubles (the written index, the argument count). */
5925
+ emitPublishedArrayGuard(arr, arrType, op, index = f64Lit(0), count = f64Lit(0)) {
5926
+ if (this.publishedTypes?.containerGuarded(arrType) !== true)
5927
+ return;
5928
+ const B = this.B;
5929
+ const ok = this.emitPublishedTest(arr);
5930
+ this.declare(`declare zeroext i1 @scr_throw_published_array(i32, ptr, double, double)`);
5931
+ const threw = B.tmp();
5932
+ B.line(`${threw} = call zeroext i1 @scr_throw_published_array(i32 ${op}, ptr ${arr}, double ${index}, double ${count})`);
5933
+ const unwind = B.newLabel("pub.throw");
5934
+ B.condBr(threw, unwind, ok);
5935
+ B.startBlock(unwind);
5936
+ this.emitUnwind();
5937
+ B.startBlock(ok);
5938
+ }
5939
+ /** The published Map/Set guard: "Cannot modify a published Map/Set". */
5940
+ emitPublishedCollectionGuard(m, t) {
5941
+ if (this.publishedTypes?.containerGuarded(t) !== true)
5942
+ return;
5943
+ const B = this.B;
5944
+ const ok = this.emitPublishedTest(m);
5945
+ this.declare(`declare void @scr_throw_published_collection(i1 zeroext)`);
5946
+ B.line(`call void @scr_throw_published_collection(i1 ${t.kind === "set" ? "true" : "false"})`);
5947
+ this.emitUnwind();
5948
+ B.startBlock(ok);
5949
+ }
5069
5950
  emitSerializationExpr(e) {
5070
5951
  return emitSerializationExpr(this, e);
5071
5952
  }
@@ -5085,6 +5966,13 @@ export class LlEmitter {
5085
5966
  return this.emitExpr(e);
5086
5967
  if (e.kind === "strLit")
5087
5968
  return { name: this.internLiteral(e.value), type: e.type };
5969
+ const guarded = guardedValue(e);
5970
+ if (guarded !== null) {
5971
+ // Throwing guards run in place, exactly as the sequence emits them.
5972
+ for (const s of e.stmts)
5973
+ this.emitStmt(s);
5974
+ return this.emitReadReceiver(guarded);
5975
+ }
5088
5976
  if (e.kind === "varRef") {
5089
5977
  this.materializeSplitLocal(e.localId);
5090
5978
  const binding = this.binding(e.localId);
@@ -5106,7 +5994,22 @@ export class LlEmitter {
5106
5994
  this.B.line(`${value} = load ptr, ptr ${binding.slot}`);
5107
5995
  return { name: value, type: e.type };
5108
5996
  }
5109
- const read = matchMapRead(e, this.unionsById);
5997
+ if (e.kind === "arrayGet" && this.canBorrowReceiver(e)) {
5998
+ // The array keeps the element alive until the consumer; a missing
5999
+ // element still traps exactly like the owned read.
6000
+ const slot = this.B.slot();
6001
+ this.B.entryAllocas.push(`${slot} = alloca ptr`);
6002
+ emitBorrowedArrayRead(this, e, slot);
6003
+ const value = this.B.tmp();
6004
+ this.B.line(`${value} = load ptr, ptr ${slot}`);
6005
+ return { name: value, type: e.type };
6006
+ }
6007
+ const wrap = this.borrowableNullableWrap(e);
6008
+ if (wrap === "unit" && e.kind === "unionWrap")
6009
+ return { name: this.unitInstanceRef(e.unionId, e.tag), type: e.type };
6010
+ if (wrap === "ref" && e.kind === "unionWrap")
6011
+ return { name: this.emitReadReceiver(e.value).name, type: e.type };
6012
+ const read = matchMapRead(e, this.boxedUnionsById);
5110
6013
  if (read) {
5111
6014
  const result = emitStackMapRead(this, read);
5112
6015
  if (result.owner)
@@ -5116,11 +6019,19 @@ export class LlEmitter {
5116
6019
  const arrayRead = this.optionalArrayReads.get(e);
5117
6020
  if (arrayRead)
5118
6021
  return emitProjectedArrayRead(this, arrayRead);
5119
- if (canStackUnion(e, this.unionsById))
6022
+ const narrow = this.checkedNarrows.get(e);
6023
+ const narrowed = narrow ? emitCheckedNarrowReceiver(this, narrow) : null;
6024
+ if (narrowed)
6025
+ return narrowed;
6026
+ if (canStackUnion(e, this.boxedUnionsById))
5120
6027
  return emitStackUnion(this, e).value;
5121
6028
  if (e.kind === "unionNarrow") {
5122
6029
  const union = this.emitUnionProjection(e.value);
5123
- return { name: this.unionPeek(union.name), type: e.type };
6030
+ return { name: this.unionPeek(union.name, e.unionId), type: e.type };
6031
+ }
6032
+ if ((e.kind === "downcast" || e.kind === "upcast") && e.value.type.kind === "object") {
6033
+ // Prefix layouts: a class cast reads the operand's own pointer.
6034
+ return { name: this.emitReadReceiver(e.value).name, type: e.type };
5124
6035
  }
5125
6036
  // A nullable-pointer field has no box to borrow. Projections use
5126
6037
  // emitUnionProjection; other borrowers get an owned heap box.
@@ -5133,6 +6044,8 @@ export class LlEmitter {
5133
6044
  return { name: this.loadRecordField(ptr, type), type: e.type };
5134
6045
  }
5135
6046
  const { ptr, type } = this.classFieldPtr(receiver.name, e.className, e.field);
6047
+ if (this.int32Slots.isField(e.className, e.field))
6048
+ return this.loadInt32Field(ptr, e.type);
5136
6049
  return { name: this.loadField(ptr, type), type: e.type };
5137
6050
  }
5138
6051
  if (e.kind === "ternary" && this.canBorrowReceiver(e.then) && this.canBorrowReceiver(e.else_)) {
@@ -5160,6 +6073,8 @@ export class LlEmitter {
5160
6073
  B.line(`${value} = load ptr, ptr ${slot}`);
5161
6074
  return { name: value, type: e.type };
5162
6075
  }
6076
+ if (e.kind === "call" && this.canBorrowReceiver(e))
6077
+ return emitBorrowedResultCall(this, e);
5163
6078
  return this.emitExpr(e);
5164
6079
  }
5165
6080
  /** A private stack union source with an independently owned payload. */
@@ -5171,8 +6086,8 @@ export class LlEmitter {
5171
6086
  isStackUnionSource(e) {
5172
6087
  if (e.type.kind !== "union")
5173
6088
  return false;
5174
- return (canStackUnion(e, this.unionsById) ||
5175
- matchMapRead(e, this.unionsById) !== null ||
6089
+ return (canStackUnion(e, this.boxedUnionsById) ||
6090
+ matchMapRead(e, this.boxedUnionsById) !== null ||
5176
6091
  this.optionalArrayReads.get(e) !== null);
5177
6092
  }
5178
6093
  /** The nullable-pointer representation behind a union-typed class field
@@ -5244,7 +6159,10 @@ export class LlEmitter {
5244
6159
  B.br(join);
5245
6160
  B.startBlock(ref);
5246
6161
  const owned = this.retainValue(p, nullable.arm);
5247
- const box = this.unionNewOwned(nullable.refTag, { name: owned, type: nullable.arm });
6162
+ const box = this.unionNewOwned(nullable.unionId, nullable.refTag, {
6163
+ name: owned,
6164
+ type: nullable.arm,
6165
+ });
5248
6166
  B.line(`store ptr ${box}, ptr ${slot}`);
5249
6167
  B.br(join);
5250
6168
  B.startBlock(join);
@@ -5256,10 +6174,10 @@ export class LlEmitter {
5256
6174
  * box, which stays owned by its existing owner. */
5257
6175
  unionToNullable(u, nullable) {
5258
6176
  const B = this.B;
5259
- const tag = this.unionTag(u);
6177
+ const tag = this.unionTag(u, nullable.unionId);
5260
6178
  const isRef = B.tmp(), sel = B.tmp();
5261
6179
  B.line(`${isRef} = icmp eq i32 ${tag}, ${nullable.refTag}`);
5262
- const payload = this.unionPeek(u);
6180
+ const payload = this.unionPeek(u, nullable.unionId);
5263
6181
  B.line(`${sel} = select i1 ${isRef}, ptr ${payload}, ptr null`);
5264
6182
  return this.retainValue(sel, nullable.arm);
5265
6183
  }
@@ -5320,20 +6238,166 @@ export class LlEmitter {
5320
6238
  isRefCounted(e.type)));
5321
6239
  }
5322
6240
  case "unionNarrow":
6241
+ // Class casts reinterpret the same pointer (prefix layouts).
6242
+ case "downcast":
6243
+ case "upcast":
5323
6244
  return this.canBorrowReceiver(e.value);
5324
6245
  case "fieldGet":
5325
6246
  case "recordGet":
5326
6247
  return this.canBorrowReceiver(e.obj);
5327
6248
  case "ternary":
5328
6249
  return this.canBorrowReceiver(e.then) && this.canBorrowReceiver(e.else_);
6250
+ case "seqExpr": {
6251
+ const guarded = guardedValue(e);
6252
+ return guarded !== null && this.canBorrowReceiver(guarded);
6253
+ }
5329
6254
  // The runtime always sets a pending exception; its unreachable
5330
6255
  // typed dummy is null and owns no receiver.
5331
6256
  case "libCall":
5332
6257
  return e.fn === "error.nodeThrow";
6258
+ case "unionWrap": {
6259
+ const wrap = this.borrowableNullableWrap(e);
6260
+ return wrap === "unit" || (wrap === "ref" && this.canBorrowReceiver(e.value));
6261
+ }
6262
+ // A borrowed-return callee preserves edges, so its result stays
6263
+ // reachable from borrowable arguments until the consumer runs.
6264
+ case "call":
6265
+ return (this.borrowedReturns.has(e.callee) &&
6266
+ e.args.every((arg) => (!isRefCounted(arg.type) || this.canBorrowReceiver(arg)) &&
6267
+ this.referenceEffects.preserves(arg)));
6268
+ // A required element read: the (borrowed) array owns the element
6269
+ // while an edge-preserving index computes.
6270
+ case "arrayGet":
6271
+ return (e.arr.type.kind === "array" &&
6272
+ isRefCounted(e.arr.type.elem) &&
6273
+ this.canBorrowReceiver(e.arr) &&
6274
+ this.referenceEffects.preserves(e.index));
6275
+ default:
6276
+ return false;
6277
+ }
6278
+ }
6279
+ /** A reference held as one plain pointer that emitReadReceiver can read
6280
+ * without owning: class instances, records, arrays, strings and
6281
+ * nullable-pointer unions. Tagged union boxes, closures and dynamic
6282
+ * values keep ordinary ownership. */
6283
+ plainReference(type) {
6284
+ switch (type.kind) {
6285
+ case "object":
6286
+ case "record":
6287
+ case "array":
6288
+ case "string":
6289
+ return true;
6290
+ case "union":
6291
+ return this.nullableUnions.has(type.unionId);
5333
6292
  default:
5334
6293
  return false;
5335
6294
  }
5336
6295
  }
6296
+ /** Functions returning a walk of their borrowed parameters return it
6297
+ * without a reference (+0) from their borrowing body; direct callers
6298
+ * either consume it as a receiver/walk or retain it at once, and the owned
6299
+ * adapter retains before releasing its parameters. Candidates preserve
6300
+ * every heap edge (so the result stays reachable from the arguments),
6301
+ * borrow every reference parameter and return plain pointers. The set
6302
+ * grows to a fixpoint because a call to a borrowed-return function with
6303
+ * walk arguments is itself a walk; walk facts are then recomputed with
6304
+ * the final set, admitting only parameters whose convention borrows. */
6305
+ analyzeBorrowedReturns() {
6306
+ const pointer = (type) => this.plainReference(type);
6307
+ const borrowedIds = (fn) => {
6308
+ const indexes = this.callLifetimes.borrowed.get(fn.name);
6309
+ return new Set(fn.params.filter((_, i) => indexes?.has(i)).map((p) => p.localId));
6310
+ };
6311
+ const hostFor = (fn) => {
6312
+ const allowed = borrowedIds(fn);
6313
+ return {
6314
+ pointerLocal: (local) => pointer(local.type),
6315
+ borrowsWithoutOwning: (e) => this.borrowsWithoutOwning(e),
6316
+ borrowedReturn: (callee) => this.borrowedReturns.has(callee),
6317
+ parameterAllowed: (id) => allowed.has(id),
6318
+ };
6319
+ };
6320
+ const preserving = [...this.fnByName.values()].filter((fn) => this.referenceEffects.functions.has(fn.name));
6321
+ const candidates = preserving.filter((fn) => {
6322
+ if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures)
6323
+ return false;
6324
+ // A borrowing vtable slot dispatches to the borrowing body (or its
6325
+ // virtual adapter) directly, and virtual callers own the result.
6326
+ if (this.implSlotBorrowed(fn.name).size > 0)
6327
+ return false;
6328
+ const indexes = this.callLifetimes.borrowed.get(fn.name);
6329
+ return (indexes !== undefined &&
6330
+ indexes.size > 0 &&
6331
+ fn.params.every((p, i) => !isRefCounted(p.type) || indexes.has(i)));
6332
+ });
6333
+ for (let changed = true; changed;) {
6334
+ changed = false;
6335
+ for (const fn of candidates) {
6336
+ if (this.borrowedReturns.has(fn.name))
6337
+ continue;
6338
+ if (!analyzeWalks(fn, hostFor(fn)).returnsWalk)
6339
+ continue;
6340
+ this.borrowedReturns.add(fn.name);
6341
+ changed = true;
6342
+ }
6343
+ }
6344
+ if (this.borrowedReturns.size === 0)
6345
+ return;
6346
+ for (const fn of preserving) {
6347
+ const walks = analyzeWalks(fn, hostFor(fn)).locals;
6348
+ if (walks.size > 0)
6349
+ this.walkBorrowsByFunction.set(fn.name, walks);
6350
+ }
6351
+ }
6352
+ /** emitReadReceiver produces this node's pointer without acquiring a
6353
+ * reference, given operands (and ternary arms) that do the same: plain
6354
+ * class/record field reads, class casts, nullable-pointer narrows and
6355
+ * wraps, and checked ternaries. Stack-boxed, map-read and boxed-field
6356
+ * sources are owned. Depends only on module facts, not the current
6357
+ * function. */
6358
+ borrowsWithoutOwning(e) {
6359
+ if (!isRefCounted(e.type) || this.isStackUnionSource(e))
6360
+ return false;
6361
+ if (matchMapRead(e, this.boxedUnionsById))
6362
+ return false;
6363
+ switch (e.kind) {
6364
+ case "fieldGet":
6365
+ return this.nullableFieldGet(e) === null;
6366
+ case "recordGet":
6367
+ return true;
6368
+ case "unionNarrow":
6369
+ return this.nullableUnions.has(e.unionId);
6370
+ case "downcast":
6371
+ case "upcast":
6372
+ return e.value.type.kind === "object";
6373
+ case "unionWrap":
6374
+ return this.borrowableNullableWrap(e) !== null;
6375
+ // The caller proves each arm separately.
6376
+ case "ternary":
6377
+ return true;
6378
+ // The caller proves the arguments are walks; their evaluation must not
6379
+ // remove an edge before the call reads them.
6380
+ case "call":
6381
+ return (this.borrowedReturns.has(e.callee) &&
6382
+ e.args.every((arg) => this.referenceEffects.preserves(arg)));
6383
+ default:
6384
+ return false;
6385
+ }
6386
+ }
6387
+ /** A wrap into a nullable-pointer union is the payload pointer itself
6388
+ * (the reference arm) or an immortal constant (a unit arm), so it can
6389
+ * borrow exactly when its payload can. Void payloads run for effects and
6390
+ * keep the ordinary path; tagged unions still construct a box. */
6391
+ borrowableNullableWrap(e) {
6392
+ if (e.kind !== "unionWrap")
6393
+ return null;
6394
+ const nullable = this.nullableUnions.get(e.unionId);
6395
+ if (!nullable)
6396
+ return null;
6397
+ if (isUnitType(e.value.type))
6398
+ return e.value.kind === "unitLit" ? "unit" : null;
6399
+ return e.tag === nullable.refTag && isRefCounted(e.value.type) ? "ref" : null;
6400
+ }
5337
6401
  materializeSplitLocal(localId) {
5338
6402
  const span = this.splitSpans.get(localId);
5339
6403
  if (span)
@@ -5420,8 +6484,8 @@ export class LlEmitter {
5420
6484
  resolveThunkFor(inner) {
5421
6485
  return resolveThunkFor(this, inner);
5422
6486
  }
5423
- tagInSet(uName, tags) {
5424
- return tagInSet(this, uName, tags);
6487
+ tagInSet(uName, unionId, tags) {
6488
+ return tagInSet(this, uName, unionId, tags);
5425
6489
  }
5426
6490
  arrPush(arr, acc, value) {
5427
6491
  return arrPush(this, arr, acc, value);
@@ -5465,8 +6529,8 @@ export class LlEmitter {
5465
6529
  emitBytesLength(elem, receiver, bytes) {
5466
6530
  return emitBytesLength(this, elem, receiver, bytes);
5467
6531
  }
5468
- emitBytesGet(elem, receiver, index, expr, inBounds = false) {
5469
- return emitBytesGet(this, elem, receiver, index, expr, inBounds);
6532
+ emitBytesGet(elem, receiver, index, expr, inBounds = false, invalidNaN = false) {
6533
+ return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN);
5470
6534
  }
5471
6535
  emitToUint32(value, expr, uint32) {
5472
6536
  return emitToUint32(this, value, expr, uint32);