@scriptc/compiler 0.2.8 → 0.2.10

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 (275) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +1 -0
  2. package/dist/backend/cycle-analysis.d.ts +56 -0
  3. package/dist/backend/cycle-analysis.js +222 -28
  4. package/dist/backend/cycle-analysis.js.map +1 -1
  5. package/dist/backend/cycle-analysis.test.js +232 -15
  6. package/dist/backend/cycle-analysis.test.js.map +1 -1
  7. package/dist/backend/cycle-mutability.d.ts +27 -6
  8. package/dist/backend/cycle-mutability.js +110 -17
  9. package/dist/backend/cycle-mutability.js.map +1 -1
  10. package/dist/backend/immutability.d.ts +57 -0
  11. package/dist/backend/immutability.js +178 -0
  12. package/dist/backend/immutability.js.map +1 -0
  13. package/dist/backend/immutability.test.d.ts +1 -0
  14. package/dist/backend/immutability.test.js +131 -0
  15. package/dist/backend/immutability.test.js.map +1 -0
  16. package/dist/backend/llvm/alloc.d.ts +13 -3
  17. package/dist/backend/llvm/alloc.js +24 -10
  18. package/dist/backend/llvm/alloc.js.map +1 -1
  19. package/dist/backend/llvm/alloc.test.js +5 -5
  20. package/dist/backend/llvm/alloc.test.js.map +1 -1
  21. package/dist/backend/llvm/borrowed-inputs.d.ts +9 -2
  22. package/dist/backend/llvm/borrowed-inputs.js +31 -3
  23. package/dist/backend/llvm/borrowed-inputs.js.map +1 -1
  24. package/dist/backend/llvm/byte-numbers.js +2 -3
  25. package/dist/backend/llvm/byte-numbers.js.map +1 -1
  26. package/dist/backend/llvm/classes.d.ts +28 -0
  27. package/dist/backend/llvm/classes.js +51 -10
  28. package/dist/backend/llvm/classes.js.map +1 -1
  29. package/dist/backend/llvm/classes.test.js +129 -1
  30. package/dist/backend/llvm/classes.test.js.map +1 -1
  31. package/dist/backend/llvm/common.d.ts +30 -0
  32. package/dist/backend/llvm/common.js +41 -0
  33. package/dist/backend/llvm/common.js.map +1 -1
  34. package/dist/backend/llvm/dense-array-access.js +6 -5
  35. package/dist/backend/llvm/dense-array-access.js.map +1 -1
  36. package/dist/backend/llvm/dense-array-access.test.js +10 -2
  37. package/dist/backend/llvm/dense-array-access.test.js.map +1 -1
  38. package/dist/backend/llvm/dyn.d.ts +6 -4
  39. package/dist/backend/llvm/dyn.js +30 -14
  40. package/dist/backend/llvm/dyn.js.map +1 -1
  41. package/dist/backend/llvm/emitter.d.ts +109 -5
  42. package/dist/backend/llvm/emitter.js +716 -112
  43. package/dist/backend/llvm/emitter.js.map +1 -1
  44. package/dist/backend/llvm/emitter.test.js +54 -3
  45. package/dist/backend/llvm/emitter.test.js.map +1 -1
  46. package/dist/backend/llvm/expr-bytes.js +35 -28
  47. package/dist/backend/llvm/expr-bytes.js.map +1 -1
  48. package/dist/backend/llvm/expr-calls.js +14 -6
  49. package/dist/backend/llvm/expr-calls.js.map +1 -1
  50. package/dist/backend/llvm/expr-containers.js +31 -8
  51. package/dist/backend/llvm/expr-containers.js.map +1 -1
  52. package/dist/backend/llvm/expr-context.d.ts +2 -0
  53. package/dist/backend/llvm/expr-control.js +46 -5
  54. package/dist/backend/llvm/expr-control.js.map +1 -1
  55. package/dist/backend/llvm/expr-dynamic.js +3 -0
  56. package/dist/backend/llvm/expr-dynamic.js.map +1 -1
  57. package/dist/backend/llvm/expr-primitives.js +30 -3
  58. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  59. package/dist/backend/llvm/expr-primitives.test.js +53 -0
  60. package/dist/backend/llvm/expr-primitives.test.js.map +1 -1
  61. package/dist/backend/llvm/expr-stream-bridges.d.ts +3 -0
  62. package/dist/backend/llvm/expr-stream-bridges.js +76 -14
  63. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  64. package/dist/backend/llvm/lib-filesystem.js +18 -2
  65. package/dist/backend/llvm/lib-filesystem.js.map +1 -1
  66. package/dist/backend/llvm/lib-shared.js +1 -0
  67. package/dist/backend/llvm/lib-shared.js.map +1 -1
  68. package/dist/backend/llvm/local-array-reads.d.ts +17 -6
  69. package/dist/backend/llvm/local-array-reads.js +157 -45
  70. package/dist/backend/llvm/local-array-reads.js.map +1 -1
  71. package/dist/backend/llvm/loop-array-borrows.d.ts +5 -2
  72. package/dist/backend/llvm/loop-array-borrows.js +27 -8
  73. package/dist/backend/llvm/loop-array-borrows.js.map +1 -1
  74. package/dist/backend/llvm/map-number-lookup.d.ts +7 -0
  75. package/dist/backend/llvm/map-number-lookup.js +18 -1
  76. package/dist/backend/llvm/map-number-lookup.js.map +1 -1
  77. package/dist/backend/llvm/map-read-lifetimes.d.ts +5 -0
  78. package/dist/backend/llvm/map-read-lifetimes.js +14 -4
  79. package/dist/backend/llvm/map-read-lifetimes.js.map +1 -1
  80. package/dist/backend/llvm/map-read-lifetimes.test.js +5 -0
  81. package/dist/backend/llvm/map-read-lifetimes.test.js.map +1 -1
  82. package/dist/backend/llvm/reference-effects.js +34 -1
  83. package/dist/backend/llvm/reference-effects.js.map +1 -1
  84. package/dist/backend/llvm/reference-writes.d.ts +124 -0
  85. package/dist/backend/llvm/reference-writes.js +1090 -0
  86. package/dist/backend/llvm/reference-writes.js.map +1 -0
  87. package/dist/backend/llvm/shapes.d.ts +22 -8
  88. package/dist/backend/llvm/shapes.js +79 -23
  89. package/dist/backend/llvm/shapes.js.map +1 -1
  90. package/dist/backend/llvm/shapes.test.js +5 -5
  91. package/dist/backend/llvm/shapes.test.js.map +1 -1
  92. package/dist/backend/llvm/string-lifetimes.test.js +39 -10
  93. package/dist/backend/llvm/string-lifetimes.test.js.map +1 -1
  94. package/dist/backend/llvm/string-reads.d.ts +3 -0
  95. package/dist/backend/llvm/string-reads.js +45 -11
  96. package/dist/backend/llvm/string-reads.js.map +1 -1
  97. package/dist/backend/llvm/string-slices.js +1 -1
  98. package/dist/backend/llvm/switch-dispatch.d.ts +2 -2
  99. package/dist/backend/llvm/switch-dispatch.js +35 -22
  100. package/dist/backend/llvm/switch-dispatch.js.map +1 -1
  101. package/dist/backend/llvm/switch-dispatch.test.js +28 -10
  102. package/dist/backend/llvm/switch-dispatch.test.js.map +1 -1
  103. package/dist/backend/llvm/trunc.d.ts +8 -0
  104. package/dist/backend/llvm/trunc.js +28 -0
  105. package/dist/backend/llvm/trunc.js.map +1 -0
  106. package/dist/backend/llvm/walk-borrows.d.ts +8 -1
  107. package/dist/backend/llvm/walk-borrows.js +15 -4
  108. package/dist/backend/llvm/walk-borrows.js.map +1 -1
  109. package/dist/backend/loop-polls.d.ts +14 -0
  110. package/dist/backend/loop-polls.js +100 -0
  111. package/dist/backend/loop-polls.js.map +1 -0
  112. package/dist/backend/loop-polls.test.d.ts +1 -0
  113. package/dist/backend/loop-polls.test.js +86 -0
  114. package/dist/backend/loop-polls.test.js.map +1 -0
  115. package/dist/backend/may-throw.d.ts +36 -2
  116. package/dist/backend/may-throw.js +328 -13
  117. package/dist/backend/may-throw.js.map +1 -1
  118. package/dist/backend/may-throw.test.js +169 -1
  119. package/dist/backend/may-throw.test.js.map +1 -1
  120. package/dist/backend/native-codegen.d.ts +1 -3
  121. package/dist/backend/native-codegen.js +4 -32
  122. package/dist/backend/native-codegen.js.map +1 -1
  123. package/dist/backend/publication.d.ts +5 -0
  124. package/dist/backend/publication.js +10 -0
  125. package/dist/backend/publication.js.map +1 -1
  126. package/dist/backend/runtime-abi.d.ts +2 -2
  127. package/dist/backend/runtime-abi.js +2 -2
  128. package/dist/backend/runtime-abi.js.map +1 -1
  129. package/dist/backend/runtime-pack-targets.test.js +1 -1
  130. package/dist/backend/runtime-pack-targets.test.js.map +1 -1
  131. package/dist/backend/runtime-pack.test.js +1 -1
  132. package/dist/backend/runtime-pack.test.js.map +1 -1
  133. package/dist/cli/command.js +8 -1
  134. package/dist/cli/command.js.map +1 -1
  135. package/dist/cli/output-options.d.ts +1 -0
  136. package/dist/cli/output-options.js +3 -0
  137. package/dist/cli/output-options.js.map +1 -1
  138. package/dist/cli/usage.d.ts +5 -1
  139. package/dist/cli/usage.js +6 -0
  140. package/dist/cli/usage.js.map +1 -1
  141. package/dist/compile-types.d.ts +5 -0
  142. package/dist/contracts/check.d.ts +8 -0
  143. package/dist/contracts/check.js +1577 -0
  144. package/dist/contracts/check.js.map +1 -0
  145. package/dist/executable/executable-cache.d.ts +2 -0
  146. package/dist/executable/executable-cache.js +3 -0
  147. package/dist/executable/executable-cache.js.map +1 -1
  148. package/dist/executable/executable-cache.test.js +1 -0
  149. package/dist/executable/executable-cache.test.js.map +1 -1
  150. package/dist/executable/prepare.js +9 -0
  151. package/dist/executable/prepare.js.map +1 -1
  152. package/dist/frontend/contract-types.d.ts +50 -0
  153. package/dist/frontend/contract-types.js +232 -0
  154. package/dist/frontend/contract-types.js.map +1 -0
  155. package/dist/frontend/lowering/array-values.js +37 -1
  156. package/dist/frontend/lowering/array-values.js.map +1 -1
  157. package/dist/frontend/lowering/builtins/strings.js +4 -1
  158. package/dist/frontend/lowering/builtins/strings.js.map +1 -1
  159. package/dist/frontend/lowering/call-arguments.js +96 -4
  160. package/dist/frontend/lowering/call-arguments.js.map +1 -1
  161. package/dist/frontend/lowering/class-method-mutations.js +88 -0
  162. package/dist/frontend/lowering/class-method-mutations.js.map +1 -1
  163. package/dist/frontend/lowering/coercions/unions.d.ts +12 -0
  164. package/dist/frontend/lowering/coercions/unions.js +63 -0
  165. package/dist/frontend/lowering/coercions/unions.js.map +1 -1
  166. package/dist/frontend/lowering/containers/bytes.js +15 -4
  167. package/dist/frontend/lowering/containers/bytes.js.map +1 -1
  168. package/dist/frontend/lowering/expressions/optional-chains.js +4 -1
  169. package/dist/frontend/lowering/expressions/optional-chains.js.map +1 -1
  170. package/dist/frontend/lowering/expressions/union-fields.d.ts +19 -0
  171. package/dist/frontend/lowering/expressions/union-fields.js +236 -0
  172. package/dist/frontend/lowering/expressions/union-fields.js.map +1 -0
  173. package/dist/frontend/lowering/lower-calls.js +21 -4
  174. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  175. package/dist/frontend/lowering/lower-exprs.d.ts +6 -0
  176. package/dist/frontend/lowering/lower-exprs.js +245 -4
  177. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  178. package/dist/frontend/lowering/lower-island.js +5 -19
  179. package/dist/frontend/lowering/lower-island.js.map +1 -1
  180. package/dist/frontend/lowering/lower-stmts.js +11 -1
  181. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  182. package/dist/frontend/lowering/lowerer.d.ts +14 -0
  183. package/dist/frontend/lowering/lowerer.js +221 -8
  184. package/dist/frontend/lowering/lowerer.js.map +1 -1
  185. package/dist/frontend/lowering/nan-coded-reads.d.ts +161 -0
  186. package/dist/frontend/lowering/nan-coded-reads.js +1342 -0
  187. package/dist/frontend/lowering/nan-coded-reads.js.map +1 -0
  188. package/dist/frontend/lowering/narrowing-stability.d.ts +100 -0
  189. package/dist/frontend/lowering/narrowing-stability.js +1174 -0
  190. package/dist/frontend/lowering/narrowing-stability.js.map +1 -0
  191. package/dist/frontend/lowering/narrowing-stability.test.d.ts +1 -0
  192. package/dist/frontend/lowering/narrowing-stability.test.js +150 -0
  193. package/dist/frontend/lowering/narrowing-stability.test.js.map +1 -0
  194. package/dist/frontend/lowering/runtime-optional-bounds.d.ts +14 -2
  195. package/dist/frontend/lowering/runtime-optional-bounds.js +17 -11
  196. package/dist/frontend/lowering/runtime-optional-bounds.js.map +1 -1
  197. package/dist/frontend/lowering/runtime-optional-bounds.test.js +28 -0
  198. package/dist/frontend/lowering/runtime-optional-bounds.test.js.map +1 -1
  199. package/dist/frontend/lowering/surfaces.js +8 -0
  200. package/dist/frontend/lowering/surfaces.js.map +1 -1
  201. package/dist/frontend/program.js +1 -1
  202. package/dist/frontend/type-mapper.js +43 -0
  203. package/dist/frontend/type-mapper.js.map +1 -1
  204. package/dist/index.d.ts +1 -1
  205. package/dist/index.js +20 -8
  206. package/dist/index.js.map +1 -1
  207. package/dist/ir/builtin-signatures.d.ts +1 -1
  208. package/dist/ir/builtin-signatures.js +2 -0
  209. package/dist/ir/builtin-signatures.js.map +1 -1
  210. package/dist/ir/constant-globals.d.ts +14 -0
  211. package/dist/ir/constant-globals.js +81 -0
  212. package/dist/ir/constant-globals.js.map +1 -0
  213. package/dist/ir/constant-globals.test.d.ts +1 -0
  214. package/dist/ir/constant-globals.test.js +97 -0
  215. package/dist/ir/constant-globals.test.js.map +1 -0
  216. package/dist/ir/final-fields.d.ts +35 -0
  217. package/dist/ir/final-fields.js +187 -0
  218. package/dist/ir/final-fields.js.map +1 -0
  219. package/dist/ir/final-fields.test.d.ts +1 -0
  220. package/dist/ir/final-fields.test.js +106 -0
  221. package/dist/ir/final-fields.test.js.map +1 -0
  222. package/dist/ir/int32-slots.d.ts +35 -0
  223. package/dist/ir/int32-slots.js +217 -104
  224. package/dist/ir/int32-slots.js.map +1 -1
  225. package/dist/ir/int32-slots.test.js +27 -0
  226. package/dist/ir/int32-slots.test.js.map +1 -1
  227. package/dist/ir/integer-loops.d.ts +5 -0
  228. package/dist/ir/integer-loops.js +23 -4
  229. package/dist/ir/integer-loops.js.map +1 -1
  230. package/dist/ir/integer-loops.test.js +43 -1
  231. package/dist/ir/integer-loops.test.js.map +1 -1
  232. package/dist/ir/integer-ranges.d.ts +20 -0
  233. package/dist/ir/integer-ranges.js +178 -21
  234. package/dist/ir/integer-ranges.js.map +1 -1
  235. package/dist/ir/integer-ranges.test.js +148 -3
  236. package/dist/ir/integer-ranges.test.js.map +1 -1
  237. package/dist/ir/ir.d.ts +38 -1
  238. package/dist/ir/ir.js +4 -0
  239. package/dist/ir/ir.js.map +1 -1
  240. package/dist/ir/serialize.d.ts +17 -3
  241. package/dist/ir/serialize.js +198 -39
  242. package/dist/ir/serialize.js.map +1 -1
  243. package/dist/ir/serialize.test.js +65 -1
  244. package/dist/ir/serialize.test.js.map +1 -1
  245. package/dist/ir/unshare.d.ts +14 -0
  246. package/dist/ir/unshare.js +60 -0
  247. package/dist/ir/unshare.js.map +1 -0
  248. package/dist/ir/unshare.test.d.ts +1 -0
  249. package/dist/ir/unshare.test.js +115 -0
  250. package/dist/ir/unshare.test.js.map +1 -0
  251. package/dist/library/prepare.js +9 -0
  252. package/dist/library/prepare.js.map +1 -1
  253. package/dist/native/cache.d.ts +8 -1
  254. package/dist/native/cache.js +38 -1
  255. package/dist/native/cache.js.map +1 -1
  256. package/dist/native/cache.test.js +11 -0
  257. package/dist/native/cache.test.js.map +1 -1
  258. package/dist/native/compiler.js +4 -3
  259. package/dist/native/compiler.js.map +1 -1
  260. package/dist/native/prepare.d.ts +3 -1
  261. package/dist/native/prepare.js +63 -28
  262. package/dist/native/prepare.js.map +1 -1
  263. package/dist/native/prepare.test.js +23 -1
  264. package/dist/native/prepare.test.js.map +1 -1
  265. package/dist/native/text-file.d.ts +10 -0
  266. package/dist/native/text-file.js +41 -0
  267. package/dist/native/text-file.js.map +1 -0
  268. package/dist/text-parts.d.ts +5 -0
  269. package/dist/text-parts.js +36 -0
  270. package/dist/text-parts.js.map +1 -0
  271. package/package.json +20 -20
  272. package/surface-manifest.json +7 -1
  273. package/dist/frontend/lowering/expressions/union-field-write.d.ts +0 -8
  274. package/dist/frontend/lowering/expressions/union-field-write.js +0 -108
  275. package/dist/frontend/lowering/expressions/union-field-write.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { emitLlvmLayouts } from "./layouts.js";
2
- import { inlineAllocSupported } from "./alloc.js";
2
+ import { FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
3
3
  import { llvmBytes as llBytes } from "../literals.js";
4
4
  import { InternalCompilerError } from "../../errors.js";
5
5
  /** Lower typed IR to LLVM, using the runtime's C ABI.
@@ -23,6 +23,7 @@ import { createHash } from "node:crypto";
23
23
  import { deflateRawSync } from "node:zlib";
24
24
  import { endsWithJump, matchStringSelfConcat, streamTypedRefEligible, undefinedArmTag, } from "../../ir/analysis.js";
25
25
  import { emitLibraryIdentityLines } from "../library-identity-markers.js";
26
+ import { loopTestsEveryIteration } from "../loop-polls.js";
26
27
  import { CAUGHT, ffiCallbackType, isDynTypedRefType, isFfiContextParam, isRefCounted, isUnitType, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeEquals, typeKey, STRING, VOID, } from "../../ir/ir.js";
27
28
  import { moduleRuntimeFeatures, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, NPM_COMPRESS_MIN, } from "../../ir/runtime-features.js";
28
29
  import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, matchIntegerCountedForLoop, } from "../../ir/integer-loops.js";
@@ -35,7 +36,10 @@ import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
35
36
  import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
36
37
  import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
37
38
  import { analyzeIntegerRanges, INT32_RANGE } from "../../ir/integer-ranges.js";
39
+ import { unshareNodes } from "../../ir/unshare.js";
38
40
  import { analyzeInt32Slots } from "../../ir/int32-slots.js";
41
+ import { analyzeFinalFields, constructorClass } from "../../ir/final-fields.js";
42
+ import { foldConstantGlobals } from "../../ir/constant-globals.js";
39
43
  import { findIntegerViews } from "./integer-views.js";
40
44
  import { findInitializerBindings, withInitializerBindings } from "../../ir/initializer-bindings.js";
41
45
  import { findConstantNumericTables } from "../../ir/constant-tables.js";
@@ -53,10 +57,11 @@ import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js"
53
57
  import { emitDenseArrayGet, emitDenseArraySet } from "./dense-array-access.js";
54
58
  import { emitStackMapRead, findMapReadLifetimes, matchMapRead, } from "./map-read-lifetimes.js";
55
59
  import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
56
- import { emitBorrowedInput } from "./borrowed-inputs.js";
60
+ import { borrowableInputs, emitBorrowedInput } from "./borrowed-inputs.js";
57
61
  import { analyzeWalks, findWalkBorrows } from "./walk-borrows.js";
58
62
  import { findReboundParameters } from "./rebound-parameters.js";
59
63
  import { ReferenceEffects } from "./reference-effects.js";
64
+ import { ReferenceWrites } from "./reference-writes.js";
60
65
  import { LlvmDebugInfo } from "./debug-info.js";
61
66
  import { StackCallbacks } from "./stack-callbacks.js";
62
67
  import { findLoopArrayBorrows } from "./loop-array-borrows.js";
@@ -64,7 +69,7 @@ import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callba
64
69
  import { findScalarStringSlices, emitStringSliceSnapshot, } from "./string-slices.js";
65
70
  import { LazyCaptures } from "./lazy-captures.js";
66
71
  import { StackCaptures } from "./stack-captures.js";
67
- import { ABSENT_FIELD_PAYLOAD, emitFieldAbsentTest, f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText, } from "./common.js";
72
+ import { ABSENT_FIELD_PAYLOAD, emitFieldAbsentTest, exactIndexLines, f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText, } from "./common.js";
68
73
  import { ffiExtendsNarrowIntegers } from "../targets.js";
69
74
  import { emitLiteralExpr, emitOperatorExpr, emitStringExpr, emitContainerExpr, emitRecordExpr, } from "./expr-primitives.js";
70
75
  import { emitControlExpr } from "./expr-control.js";
@@ -72,6 +77,7 @@ import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
72
77
  import { emitDynamicExpr } from "./expr-dynamic.js";
73
78
  import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js";
74
79
  import { PublishWalkers } from "./publish.js";
80
+ import { computeImmutableFields, stableGlobals } from "../immutability.js";
75
81
  import { computePublishedTypes } from "../publication.js";
76
82
  import { emitJsInteropExpr, emitExpr } from "./expr-dispatch.js";
77
83
  import { emitJsMarshal, emitJsOp, emitJsExit, islandAdapter, islandTypedAdapter, } from "./expr-island.js";
@@ -94,7 +100,7 @@ import { LlWalkers } from "./walkers.js";
94
100
  import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js";
95
101
  import { emitNullablePresent, emitUnionPeek, emitUnionTag } from "./union-repr.js";
96
102
  import { literalCapWord, literalCapWord32 } from "./string-key-hash.js";
97
- import { NUMBER_MAP_ENTRY_DECL } from "./map-number-lookup.js";
103
+ import { NUMBER_MAP_ENTRY_DECL, STRING_MAP_ENTRY_DECL } from "./map-number-lookup.js";
98
104
  import { STRING_READ_HELPERS } from "./string-reads.js";
99
105
  import { emitConcatInputs, emitMixedConcat, numberPart, stringParts, } from "./string-construction.js";
100
106
  import { arrNewCall, boxAccess, boxNewCall, boxReleaseSym, computeTraced, elemAccess, emitInlineRcHelpers, emitNullableRetainWrappers, FN_ATTRS, inlineRcDecls, llFieldType, releaseSym, retainSym, traceArg, typedRefConstructor, vAdapters, } from "./shapes.js";
@@ -124,16 +130,20 @@ function ffiCallbackDummyLl(callback) {
124
130
  return "i32 0";
125
131
  }
126
132
  }
127
- export function emitLlvmModule(mod, options = {}) {
133
+ export function emitLlvmModule(input, options = {}) {
134
+ // Facts are keyed by node identity: emit the same module whether it was
135
+ // lowered in memory or read back from its serialized form.
136
+ const mod = unshareNodes(input);
128
137
  // Keep source storage intact for debugger inspection in dev builds.
129
138
  return new LlEmitter(options.debugSources === undefined
130
- ? scalarizeNumericRecords(specializeNumericCalls(mod, mangleFunction))
139
+ ? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
131
140
  : mod, options).emit();
132
141
  }
133
142
  /** Keep large translation units below the host's single-string limit. */
134
- export function emitLlvmModuleSource(mod, options = {}) {
143
+ export function emitLlvmModuleSource(input, options = {}) {
144
+ const mod = unshareNodes(input);
135
145
  const parts = new LlEmitter(options.debugSources === undefined
136
- ? scalarizeNumericRecords(specializeNumericCalls(mod, mangleFunction))
146
+ ? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
137
147
  : mod, options).emitParts();
138
148
  const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1));
139
149
  if (length <= 256 * 1024 * 1024)
@@ -224,12 +234,27 @@ export class LlEmitter {
224
234
  debugScope = null;
225
235
  sizeType;
226
236
  ffiExtendNarrowIntegers;
227
- cycleColorOffset;
237
+ cycleBufferedOffset;
238
+ /** Emitted ScrCycType descriptors so far (their collector statistics
239
+ * indices; see ShapeHost.nextCycKind). */
240
+ cycKinds = 0;
228
241
  wasi;
242
+ /** The target lacks a rounding instruction at its baseline CPU (x86-64
243
+ * before SSE4.1), so `llvm.trunc` would lower to a libm call: emit
244
+ * truncation inline through an integer round trip instead. */
245
+ inlineTrunc;
229
246
  /** ELF worker executables give thread-locals the executable TLS models. */
230
247
  executableTls;
248
+ /** Source loops enclosing the statement being emitted in the current
249
+ * function body. */
250
+ loopDepth = 0;
251
+ /** Darwin worker executables read the stack guard from a pthread
252
+ * thread-specific-data slot off the thread pointer (scr_runtime.h's
253
+ * SCR_STACK_GUARD_TSD): the architecture names the access sequence. */
254
+ stackGuardTsd;
231
255
  emitLibraryIdentity;
232
256
  runtimeAbiMarker;
257
+ detachedExit;
233
258
  /** Interned string literals: UTF-8 text → { symbol, byte length } —
234
259
  * first-use order, the runtime ABI’s determinism discipline. */
235
260
  literals = new Map();
@@ -284,6 +309,12 @@ export class LlEmitter {
284
309
  * fiberless-async analysis scans it for suspension entry points. */
285
310
  fnDefText = new Map();
286
311
  referenceEffects;
312
+ /** Field-sensitive reference writes (null in debug builds). */
313
+ referenceWrites = null;
314
+ borrowedReturnKeyCache = new Map();
315
+ /** The statement list being emitted and the current index, for blocks
316
+ * whose bindings end with the list (emitBlock and function bodies). */
317
+ stmtCursor = null;
287
318
  optionalArrayReads;
288
319
  checkedNarrows;
289
320
  callArrayReads = new Map();
@@ -327,6 +358,9 @@ export class LlEmitter {
327
358
  ffiHasRetainedCallback;
328
359
  ffiHasForeignCallback;
329
360
  globalTypes = new Map();
361
+ /** Module constants that keep one value once initialized
362
+ * (immutability.ts stableGlobals). */
363
+ stableGlobals = new Set();
330
364
  tdzGlobals = new Map();
331
365
  constantNumericTables;
332
366
  /** May-throw analysis (computeMayThrow): pending
@@ -342,6 +376,13 @@ export class LlEmitter {
342
376
  /** Static types whose values can be published (@scriptc/threads): their
343
377
  * stores carry the frozen-object guard. Null without publish calls. */
344
378
  publishedTypes;
379
+ /** Construction-only class and record fields (immutability.ts); null
380
+ * where the facts are disabled. */
381
+ immutableFields;
382
+ /** Constructor function names: their bodies may still write their own
383
+ * `this`'s construction-only fields. */
384
+ constructorFunctions;
385
+ inConstructor = false;
345
386
  /** Method names with at least one may-throw implementation — the
346
387
  * virtualCall pending check's key. */
347
388
  mayThrowMethods = new Set();
@@ -418,11 +459,22 @@ export class LlEmitter {
418
459
  initializerBindings;
419
460
  /** Whole-program int32 storage and parameter/result facts. */
420
461
  int32Slots;
462
+ /** Immutable module constants (contract types) whose graphs become
463
+ * immortal once initialized: their reads need no reference counting. */
464
+ immortalGlobals;
465
+ /** ProgramLifetime hierarchy roots (contract types): their instances are
466
+ * immortal from construction (classes.ts programLifetimeClass). */
467
+ programLifetime;
421
468
  numericLocals = new Map();
422
469
  captureIds = new Set();
423
470
  /** Active induction bindings retain their width independently of size_t. */
424
471
  integerLoopBindings = new Map();
425
472
  countedLoopsEnabled = false;
473
+ /** Whole-program final reference fields (ir/final-fields.ts). */
474
+ finalFields;
475
+ /** The current function may borrow final fields of borrowable objects:
476
+ * not a constructor (where they change), not async or a generator. */
477
+ finalFieldBorrows = false;
426
478
  integerRanges = new Map();
427
479
  byteWindowsEnabled = true;
428
480
  byteWindowEntries = new Map();
@@ -472,22 +524,43 @@ export class LlEmitter {
472
524
  this.constantNumericTables = findConstantNumericTables(mod);
473
525
  this.initializerBindings = findInitializerBindings(mod);
474
526
  this.int32Slots = analyzeInt32Slots(mod);
527
+ this.immortalGlobals = new Set(mod.contracts?.immortalGlobals ?? []);
528
+ this.programLifetime = new Set(mod.contracts?.programLifetime ?? []);
529
+ this.finalFields = analyzeFinalFields(mod);
475
530
  this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
476
531
  this.ffiExtendNarrowIntegers =
477
532
  options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
478
533
  this.wasi = options.wasi === true;
534
+ // An empty triple targets the host, as for executableTls below: the
535
+ // Node-hosted CLI passes none, the native CLI passes its toolchain's, and
536
+ // both must emit the same module for the same target.
537
+ this.inlineTrunc =
538
+ !this.wasi &&
539
+ (options.targetTriple
540
+ ? /^(x86_64|amd64)\b/i.test(options.targetTriple)
541
+ : process.arch === "x64");
479
542
  this.executableTls =
480
543
  mod.workers === true &&
481
544
  mod.lib === undefined &&
482
545
  !this.wasi &&
483
546
  !/apple|darwin|windows|mingw|cygwin|win32/.test(options.targetTriple || process.platform);
547
+ const host = options.targetTriple || `${process.arch}-${process.platform}`;
548
+ this.stackGuardTsd =
549
+ mod.workers === true && mod.lib === undefined && !this.wasi && /apple|darwin/.test(host)
550
+ ? /^(arm64|aarch64)/.test(host)
551
+ ? "aarch64"
552
+ : /^(x86_64|amd64|x64)/.test(host)
553
+ ? "x86_64"
554
+ : null
555
+ : null;
484
556
  this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
485
557
  this.runtimeAbiMarker = options.runtimeAbiMarker === true;
558
+ this.detachedExit = options.detachedExit === true;
486
559
  this.rcHelpers = options.inlineRc === true ? new Set() : null;
487
- // ScrCycHdr.color stays 12 bytes behind a wasm32 object and 16 bytes
488
- // behind a 64-bit object. The wasm32 header pads before color so its
489
- // payload remains double-aligned without changing this ABI offset.
490
- this.cycleColorOffset = options.pointerBits === 32 ? 12 : 16;
560
+ // ScrCycHdr.buffered sits 6 bytes behind every object. The wasm32
561
+ // header pads before its descriptor so its payload remains
562
+ // double-aligned without changing this ABI offset.
563
+ this.cycleBufferedOffset = 6;
491
564
  this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []);
492
565
  this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
493
566
  this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []);
@@ -507,6 +580,8 @@ export class LlEmitter {
507
580
  this.workerEntryPolls = mt.workerEntryPolls ?? null;
508
581
  this.stackChecks = mt.stackChecks ?? null;
509
582
  this.publishedTypes = computePublishedTypes(mod);
583
+ this.immutableFields = computeImmutableFields(mod);
584
+ this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
510
585
  this.indirectMayThrow = mt.indirect || mod.workers === true;
511
586
  for (const cls of mod.classes ?? []) {
512
587
  for (const m of cls.methods ?? []) {
@@ -522,9 +597,12 @@ export class LlEmitter {
522
597
  this.nullableFields = new NullableRefFields(mod.classes ?? [], this.boxedUnionsById);
523
598
  // Optional array reads also cover nullable unions: emitLocalArrayRead
524
599
  // then yields the (borrowed or owned) element pointer, not a stack box.
525
- this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById);
600
+ // Arrays of such a union are read the same way.
601
+ this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById, (id) => this.nullableUnions.has(id));
526
602
  this.checkedNarrows = new CheckedNarrows(this.fnByName);
527
603
  this.referenceEffects = new ReferenceEffects(this.fnByName, (call) => this.optionalArrayReads.get(call) !== null, mod.classes ?? []);
604
+ if (this.debug === null)
605
+ this.referenceWrites = new ReferenceWrites(this.fnByName, mod.classes ?? [], (call) => this.optionalArrayReads.get(call) !== null, new Set((mod.globals ?? []).map((g) => g.id)), { unions: mod.unions, records: mod.records }, this.immutableFields);
528
606
  const walkParameters = new Map();
529
607
  if (this.debug === null) {
530
608
  const host = {
@@ -532,9 +610,10 @@ export class LlEmitter {
532
610
  borrowsWithoutOwning: (e) => this.borrowsWithoutOwning(e),
533
611
  };
534
612
  for (const fn of this.fnByName.values()) {
535
- if (!this.referenceEffects.functions.has(fn.name))
613
+ const preserving = this.referenceEffects.functions.has(fn.name);
614
+ if (!preserving && this.referenceWrites === null)
536
615
  continue;
537
- const walks = findWalkBorrows(fn, host);
616
+ const walks = findWalkBorrows(fn, preserving ? host : this.unwrittenWalkHost(fn, host));
538
617
  if (walks.size === 0)
539
618
  continue;
540
619
  this.walkBorrowsByFunction.set(fn.name, walks);
@@ -578,7 +657,7 @@ export class LlEmitter {
578
657
  this.tracedUnions = traced.unions;
579
658
  this.shapeHost = {
580
659
  sizeType: this.sizeType,
581
- cycleColorOffset: this.cycleColorOffset,
660
+ cycleBufferedOffset: this.cycleBufferedOffset,
582
661
  tracedShapes: this.tracedShapes,
583
662
  tracedUnions: this.tracedUnions,
584
663
  nullableUnions: this.nullableUnions,
@@ -595,6 +674,7 @@ export class LlEmitter {
595
674
  // paths address its thread-local state.
596
675
  threadLocalAlloc: mod.workers === true,
597
676
  rcHelpers: this.rcHelpers,
677
+ nextCycKind: () => FIRST_EMITTED_CYC_KIND + this.cycKinds++,
598
678
  unionsById: this.unionsById,
599
679
  declare: (decl) => this.declare(decl),
600
680
  needOom: () => this.needOom(),
@@ -617,6 +697,7 @@ export class LlEmitter {
617
697
  },
618
698
  pendingTestLines: (dest) => this.pendingTestLines(dest),
619
699
  virtualEntry: (implFn) => this.virtualEntry(implFn),
700
+ programLifetime: this.programLifetime,
620
701
  };
621
702
  this.walkers = new LlWalkers(this.shapeHost);
622
703
  this.dyn = new LlDyn(this.shapeHost);
@@ -638,6 +719,7 @@ export class LlEmitter {
638
719
  if (g.tdz)
639
720
  this.tdzGlobals.set(g.id, g.name);
640
721
  }
722
+ this.stableGlobals = stableGlobals(mod);
641
723
  // Runtime error, EventEmitter, and stream classes have known layouts.
642
724
  // Subclasses embed the ScrEmitter/ScrStream prefixes from classes.ts.
643
725
  const classes = mod.classes ?? [];
@@ -997,14 +1079,16 @@ export class LlEmitter {
997
1079
  if (!this.executableTls)
998
1080
  return parts;
999
1081
  // 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.
1082
+ // TLS block: the program's internal ones, and the runtime's external
1083
+ // ones, because the runtime is always linked statically into the
1084
+ // executable. So on ELF they can all use the local-exec model: LLVM's
1085
+ // default general-dynamic sequence (relaxed by the linker to `mov %fs:0`
1086
+ // plus an add), and even initial-exec (relaxed to `mov $offset` plus an
1087
+ // indexed access), become one %fs-relative operand LLVM can fold into
1088
+ // loads and compares. Darwin's thread-local variables have one model.
1005
1089
  return parts.map((part) => part
1006
1090
  .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 "));
1091
+ .replace(/^(@[-$._A-Za-z0-9]+ = external )thread_local global /gm, "$1thread_local(localexec) global "));
1008
1092
  }
1009
1093
  emitModuleParts() {
1010
1094
  // Function bodies first (the literal/unit/fn-value tables fill as they
@@ -1356,10 +1440,15 @@ export class LlEmitter {
1356
1440
  `%ScrClosure = type { ${this.sizeType}, ptr, ${this.sizeType}, ptr, i32, i32, ptr }`,
1357
1441
  `%ScrFfiTable = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, i8, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`,
1358
1442
  `%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr, double, ptr, ptr }`,
1359
- // ScrArr mirrors scr_runtime.h field-for-field. Live dynamic stream
1360
- // commits swap its mutable dense, sparse, presence, and property
1361
- // storage while preserving the target object's identity.
1362
- `%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, ptr, ptr, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ptr, ${this.sizeType}, ${this.sizeType}, ptr }`,
1443
+ // ScrArr mirrors scr_runtime.h field-for-field: { rc, len, cap, elem,
1444
+ // cyc, ops, data, present, ext } (field numbers in SCR_ARR_FIELD),
1445
+ // where ops points at the element type's constant RC descriptor
1446
+ // (%ScrArrOps, SCR_ELEM_REF only) and ext at the out-of-line sparse,
1447
+ // property and metadata storage (NULL until first used). Live dynamic
1448
+ // stream commits swap its dense, presence and out-of-line storage while
1449
+ // preserving the target object's identity.
1450
+ `%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr }`,
1451
+ `%ScrArrOps = type { ptr, ptr, ptr }`,
1363
1452
  // The ScrMap prefix { rc, key_kind, val_kind, val_retain, val_release,
1364
1453
  // val_trace, key_retain, key_release, key_trace } — the inline RC
1365
1454
  // fast paths test the two trace slots (shapes.ts).
@@ -1372,9 +1461,13 @@ export class LlEmitter {
1372
1461
  ...(this.decls.has(NUMBER_MAP_ENTRY_DECL)
1373
1462
  ? [
1374
1463
  `%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
1464
  ]
1377
1465
  : []),
1466
+ // String-keyed reference reads load the value slot of the entry
1467
+ // scr_map_entry_str returns (map-number-lookup.ts).
1468
+ ...(this.decls.has(NUMBER_MAP_ENTRY_DECL) || this.decls.has(STRING_MAP_ENTRY_DECL)
1469
+ ? [`%ScrMapEntry = type { i64, i64, i64 }`]
1470
+ : []),
1378
1471
  // The runtime error prefix { rc, vt, name, message, code, cause } and the
1379
1472
  // class-object shape { rc, pre, post, ctor, name } — field reads on
1380
1473
  // builtin errors and classval loads GEP through these.
@@ -1422,6 +1515,10 @@ export class LlEmitter {
1422
1515
  const tl = this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : "";
1423
1516
  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
1424
1517
  ? [`declare void @${RUNTIME_ABI_MARKER}()`]
1518
+ : []),
1519
+ // Overrides the runtime's weak default (scr_lib.c).
1520
+ ...(this.detachedExit && this.mod.lib === undefined
1521
+ ? [`@scr_detached_exit = constant i8 1`]
1425
1522
  : []));
1426
1523
  for (const d of this.decls)
1427
1524
  out.push(d);
@@ -1429,10 +1526,9 @@ export class LlEmitter {
1429
1526
  const flushedDecls = new Set(this.decls);
1430
1527
  for (const table of this.constantNumericTables.values()) {
1431
1528
  const n = table.values.length;
1432
- out.push(`@${table.symbol} = private constant [${n} x double] [${table.values.map((v) => `double ${f64Lit(v)}`).join(", ")}]`, `define internal double @${table.symbol}_get(ptr %a, double %i) alwaysinline #0 {`, `entry:`, ` %initialized = icmp ne ptr %a, null`, ` %nonnegative = fcmp oge double %i, ${f64Lit(0)}`, ` %below = fcmp olt double %i, ${f64Lit(n)}`, ` %range = and i1 %nonnegative, %below`, ` %safe = and i1 %initialized, %range`, ` br i1 %safe, label %convert, label %fallback`, `convert:`,
1433
- // The branch must dominate fptoui: NaN/out-of-range conversion
1434
- // would produce poison. Fractional indices also need a fallback.
1435
- ` %index = fptoui double %i to ${this.sizeType}`, ` %roundtrip = uitofp ${this.sizeType} %index to double`, ` %integer = fcmp oeq double %roundtrip, %i`, ` br i1 %integer, label %read, label %fallback`, `read:`, ` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, ${this.sizeType} 0, ${this.sizeType} %index`, ` %value = load double, ptr %slot`, ` ret double %value`, `fallback:`, ` %generic = call double @scr_arr_get_number(ptr %a, double %i)`, ` ret double %generic`, `}`, ``);
1529
+ let tableTmp = 0;
1530
+ const tableIndex = exactIndexLines("%i", String(n), "i64", () => `%ix${tableTmp++}`);
1531
+ out.push(`@${table.symbol} = private constant [${n} x double] [${table.values.map((v) => `double ${f64Lit(v)}`).join(", ")}]`, `define internal double @${table.symbol}_get(ptr %a, double %i) alwaysinline #0 {`, `entry:`, ` %initialized = icmp ne ptr %a, null`, ...tableIndex.lines.map((line) => ` ${line}`), ` %safe = and i1 %initialized, ${tableIndex.ok}`, ` br i1 %safe, label %read, label %fallback`, `read:`, ` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, i64 0, i64 ${tableIndex.wide}`, ` %value = load double, ptr %slot`, ` ret double %value`, `fallback:`, ` %generic = call double @scr_arr_get_number(ptr %a, double %i)`, ` ret double %generic`, `}`, ``);
1436
1532
  }
1437
1533
  for (const [text, lit] of this.literals) {
1438
1534
  // Immortal interned ScrStr: { rc = SIZE_MAX, len, cap = len, bytes\0 } —
@@ -1499,7 +1595,7 @@ export class LlEmitter {
1499
1595
  // strings — a no-op.
1500
1596
  const n = inst.slots.length;
1501
1597
  const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`;
1502
- out.push(`@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`, `@${inst.sym}_present = internal constant [${n} x i8] ${present}`, `@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, ptr null, ptr null, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null, ${this.sizeType} 0, ${this.sizeType} 0, ptr null, ${this.sizeType} 0, ${this.sizeType} 0, ptr null }`);
1598
+ out.push(`@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`, `@${inst.sym}_present = internal constant [${n} x i8] ${present}`, `@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, i32 0, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`);
1503
1599
  }
1504
1600
  if (this.templateStringsInstances.size > 0)
1505
1601
  out.push(``);
@@ -2024,7 +2120,13 @@ export class LlEmitter {
2024
2120
  // keeps the complete SameValueZero semantics.
2025
2121
  if (this.decls.has(NUMBER_MAP_ENTRY_DECL)) {
2026
2122
  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`, `}`, ``);
2123
+ let mapTmp = 0;
2124
+ const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`);
2125
+ 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`,
2126
+ // A non-integral key inside the table is as absent as one outside it.
2127
+ ...mapIndex.lines.map((line) => ` ${line}`), ` br i1 ${mapIndex.ok}, label %slot, label %outside`, `slot:`, sz === "i64"
2128
+ ? ` %i = add i64 ${mapIndex.wide}, 0`
2129
+ : ` %i = trunc i64 ${mapIndex.wide} to ${sz}`, ` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 18`, ` %dense = load ptr, ptr %dp`, ` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`, ` %s = load i32, ptr %sp`, ` %empty = icmp eq i32 %s, 0`, ` br i1 %empty, label %miss, label %hit`, `hit:`, ` %e32 = sub i32 %s, 1`, sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`, ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 12`, ` %entries = load ptr, ptr %ep`, ` %found = getelementptr inbounds %ScrMapEntry, ptr %entries, ${sz} %e`, ` ret ptr %found`, `miss:`, ` ret ptr null`, `outside:`, ` %dense.any = icmp ne ${sz} %nd, 0`, ` br i1 %dense.any, label %miss, label %slow`, `slow:`, ` %r = call ptr @scr_map_entry_f64(ptr %m, double %k)`, ` ret ptr %r`, `}`, ``);
2028
2130
  }
2029
2131
  // String comparisons enter through small internal wrappers that LLVM
2030
2132
  // inlines at each site: interned literals and shared values compare by
@@ -2833,13 +2935,28 @@ export class LlEmitter {
2833
2935
  * return and stays eligible for tail-call and recursion elimination. */
2834
2936
  emitUnwind(passThrough) {
2835
2937
  if (this.mod.workers && this.tryStack.length > 0) {
2836
- this.declare(`declare zeroext i1 @scr_context_stopping()`);
2837
- const stopped = this.B.tmp();
2838
- this.B.line(`${stopped} = call zeroext i1 @scr_context_stopping()`);
2839
2938
  const stop = this.B.newLabel("context.stop");
2840
2939
  const ordinary = this.B.newLabel("context.throw");
2841
- this.B.condBr(stopped, stop, ordinary);
2842
- this.B.startBlock(stop);
2940
+ if (this.executableTls) {
2941
+ // A set stop bit unwinds while the active cell may still be clear:
2942
+ // install the termination sentinel before skipping the handlers.
2943
+ const word = this.loadExcFlag();
2944
+ const bit = this.B.tmp();
2945
+ const stopped = this.B.tmp();
2946
+ this.B.line(`${bit} = and i32 ${word}, 2`);
2947
+ this.B.line(`${stopped} = icmp ne i32 ${bit}, 0`);
2948
+ this.B.condBr(stopped, stop, ordinary);
2949
+ this.B.startBlock(stop);
2950
+ this.declare(`declare zeroext i1 @scr_context_checkpoint()`);
2951
+ this.B.line(`call zeroext i1 @scr_context_checkpoint()`);
2952
+ }
2953
+ else {
2954
+ this.declare(`declare zeroext i1 @scr_context_stopping()`);
2955
+ const stopped = this.B.tmp();
2956
+ this.B.line(`${stopped} = call zeroext i1 @scr_context_stopping()`);
2957
+ this.B.condBr(stopped, stop, ordinary);
2958
+ this.B.startBlock(stop);
2959
+ }
2843
2960
  const handlers = this.tryStack;
2844
2961
  this.tryStack = [];
2845
2962
  this.emitUnwind();
@@ -2896,6 +3013,18 @@ export class LlEmitter {
2896
3013
  }
2897
3014
  this.B.br(cleanup.label);
2898
3015
  }
3016
+ /** Loads the running thread's exact pending word (ELF worker
3017
+ * executables, scr_exception.c): bit 0 mirrors the active cell, bit 1
3018
+ * requests the context's stop. It lives in the executable's static TLS
3019
+ * block, so the access is one thread-pointer-relative load. Monotonic by
3020
+ * default: a loop must not hoist the load out and miss a cross-thread
3021
+ * stop (see emitPendingCheck for the unordered tests). */
3022
+ loadExcFlag(unordered = false) {
3023
+ this.declare(`@scr_exc_flag = external thread_local(localexec) global i32, align 4`);
3024
+ const word = this.B.tmp();
3025
+ this.B.line(`${word} = load atomic i32, ptr @scr_exc_flag ${unordered ? "unordered" : "monotonic"}, align 4`);
3026
+ return word;
3027
+ }
2899
3028
  /** Instructions (two-space indented) that set the i1 `dest` to "an
2900
3029
  * exception is pending in the active context" — scr_exc_pending()'s
2901
3030
  * meaning — for the helper and adapter bodies that test it outside
@@ -2922,8 +3051,9 @@ export class LlEmitter {
2922
3051
  * call's result temp must join its frame BEFORE this runs so the unwind
2923
3052
  * releases the dummy (NULL for refcounted kinds) harmlessly. A scalar
2924
3053
  * call result of the function's return type may be passed as the unwind's
2925
- * dummy value (see emitUnwind). */
2926
- emitPendingCheck(passThrough) {
3054
+ * dummy value (see emitUnwind). `poll` marks a worker termination poll
3055
+ * (function entry or loop budget), whose test must stay ordered. */
3056
+ emitPendingCheck(passThrough, poll = false) {
2927
3057
  const through = passThrough !== undefined &&
2928
3058
  this.llType(passThrough.type) === this.llType(this.currentReturnType) &&
2929
3059
  (passThrough.type.kind === "f64" || passThrough.type.kind === "bool")
@@ -2945,16 +3075,35 @@ export class LlEmitter {
2945
3075
  return;
2946
3076
  }
2947
3077
  // 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.
3078
+ // field). ELF worker executables test the thread's exact pending word,
3079
+ // which is also nonzero once the context must stop, and unwind directly:
3080
+ // no site carries an out-of-line call, so LLVM's inliner sees the check
3081
+ // as two instructions. Other worker programs test the process-wide
3082
+ // alert word: it is nonzero while any script thread may have an
3083
+ // exception pending in its active cell or has been asked to stop, so
3084
+ // the common case is one ordinary global load (a thread-local access is
3085
+ // a call on Darwin) and the out-of-line scr_exc_pending answers for this
3086
+ // thread, observing cancellation and reinstalling the termination
3087
+ // sentinel.
2954
3088
  this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
2955
3089
  let word;
2956
- if (this.mod.workers === true) {
2957
- this.declare(`@scr_exc_alert = external global i32, align 64`);
3090
+ const exact = this.mod.workers !== true || this.executableTls;
3091
+ if (this.executableTls) {
3092
+ // Termination stays observable where the polls were dropped for it:
3093
+ // polls, tests inside source loops (exc's loop-poll rule relies on
3094
+ // them every iteration) and tail-position tests (a dead ordered load
3095
+ // keeps recursion from becoming a loop without stack growth) use an
3096
+ // ordered load, which LLVM never deletes, hoists or merges. Every
3097
+ // other test is an unordered load: it reads the same word, but LLVM's
3098
+ // memory analyses no longer treat it as a write, so loads around it
3099
+ // stay forwardable after inlining. Same-thread changes only happen in
3100
+ // runtime calls, which clobber memory.
3101
+ word = this.loadExcFlag(!poll && through === undefined && this.loopDepth === 0);
3102
+ }
3103
+ else if (this.mod.workers === true) {
3104
+ // dso_local: executables link the runtime statically, so the word is
3105
+ // addressed directly instead of through the GOT.
3106
+ this.declare(`@scr_exc_alert = external dso_local global i32, align 64`);
2958
3107
  word = B.tmp();
2959
3108
  B.line(`${word} = load atomic i32, ptr @scr_exc_alert monotonic, align 64`);
2960
3109
  }
@@ -2984,7 +3133,7 @@ export class LlEmitter {
2984
3133
  B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${hit}, i1 false)`);
2985
3134
  const lu = B.newLabel("exc.u");
2986
3135
  const lk = B.newLabel("exc.k");
2987
- if (this.mod.workers === true) {
3136
+ if (!exact) {
2988
3137
  this.declare(`declare zeroext i1 @scr_exc_pending()`);
2989
3138
  const slow = B.newLabel("exc.s");
2990
3139
  B.condBr(cold, slow, lk);
@@ -3016,30 +3165,98 @@ export class LlEmitter {
3016
3165
  * throw Node's RangeError and unwind like any other pending exception. */
3017
3166
  emitStackCheck() {
3018
3167
  const B = this.B;
3019
- const tl = this.mod.workers === true ? "thread_local " : "";
3020
- this.declare(`@scr_stack_guard = external ${tl}global ptr`);
3021
3168
  this.declare(`declare ptr @llvm.stacksave.p0()`);
3022
3169
  this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
3023
3170
  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
3171
  const over = B.newLabel("stack.over");
3033
3172
  const ok = B.newLabel("stack.ok");
3034
- B.condBr(cold, over, ok);
3173
+ const sp = B.tmp();
3174
+ B.line(`${sp} = call ptr @llvm.stacksave.p0()`);
3175
+ const test = (guard) => {
3176
+ const low = B.tmp();
3177
+ const cold = B.tmp();
3178
+ B.line(`${low} = icmp ult ptr ${sp}, ${guard}`);
3179
+ B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${low}, i1 false)`);
3180
+ B.condBr(cold, over, ok);
3181
+ };
3182
+ if (this.stackGuardTsd !== null) {
3183
+ // A Darwin thread-local access is a call to its TLV thunk: read the
3184
+ // guard from its thread-specific-data slot at the runtime-validated
3185
+ // offset instead, and call out only when the runtime found no slot.
3186
+ this.declare(`@scr_stack_guard_tsd = external dso_local global i64`);
3187
+ this.declare(`declare i64 @scr_stack_guard_get()`);
3188
+ const offset = B.tmp();
3189
+ const direct = B.tmp();
3190
+ const likely = B.tmp();
3191
+ B.line(`${offset} = load i64, ptr @scr_stack_guard_tsd`);
3192
+ B.line(`${direct} = icmp ne i64 ${offset}, 0`);
3193
+ B.line(`${likely} = call i1 @llvm.expect.i1(i1 ${direct}, i1 true)`);
3194
+ const fast = B.newLabel("stack.tsd");
3195
+ const slow = B.newLabel("stack.tlv");
3196
+ B.condBr(likely, fast, slow);
3197
+ B.startBlock(fast);
3198
+ const guard = B.tmp();
3199
+ if (this.stackGuardTsd === "aarch64") {
3200
+ this.declare(`declare i64 @llvm.read_register.i64(metadata)`);
3201
+ const thread = B.tmp();
3202
+ const base = B.tmp();
3203
+ const slot = B.tmp();
3204
+ const address = B.tmp();
3205
+ B.line(`${thread} = call i64 @llvm.read_register.i64(metadata !{!"tpidrro_el0"})`);
3206
+ B.line(`${base} = and i64 ${thread}, -8`);
3207
+ B.line(`${slot} = add i64 ${base}, ${offset}`);
3208
+ B.line(`${address} = inttoptr i64 ${slot} to ptr`);
3209
+ B.line(`${guard} = load ptr, ptr ${address}`);
3210
+ }
3211
+ else {
3212
+ const address = B.tmp();
3213
+ B.line(`${address} = inttoptr i64 ${offset} to ptr addrspace(256)`);
3214
+ B.line(`${guard} = load ptr, ptr addrspace(256) ${address}`);
3215
+ }
3216
+ test(guard);
3217
+ B.startBlock(slow);
3218
+ // uintptr_t scr_stack_guard_get(void): an i64 in the C ABI.
3219
+ const raw = B.tmp();
3220
+ const fallback = B.tmp();
3221
+ B.line(`${raw} = call i64 @scr_stack_guard_get()`);
3222
+ B.line(`${fallback} = inttoptr i64 ${raw} to ptr`);
3223
+ test(fallback);
3224
+ }
3225
+ else {
3226
+ const tl = this.mod.workers === true ? "thread_local " : "";
3227
+ this.declare(`@scr_stack_guard = external ${tl}global ptr`);
3228
+ const guard = B.tmp();
3229
+ B.line(`${guard} = load ptr, ptr @scr_stack_guard`);
3230
+ test(guard);
3231
+ }
3035
3232
  B.startBlock(over);
3036
3233
  B.line(`call void @scr_stack_overflow()`);
3037
3234
  this.emitUnwind();
3038
3235
  B.startBlock(ok);
3039
3236
  }
3040
- workerLoopBudget() {
3237
+ /** Whether the emitter follows this call with a pending test (the
3238
+ * conditions in expr-calls.ts). */
3239
+ callTestsPending(e) {
3240
+ switch (e.kind) {
3241
+ case "call":
3242
+ return this.mayThrow.has(e.callee);
3243
+ case "new":
3244
+ return this.mayThrow.has(`%${e.className}.constructor`);
3245
+ case "virtualCall":
3246
+ return this.mayThrowMethods.has(e.method);
3247
+ case "callValue":
3248
+ return this.indirectMayThrow;
3249
+ default:
3250
+ return false;
3251
+ }
3252
+ }
3253
+ workerLoopBudget(loop) {
3041
3254
  if (!this.mod.workers)
3042
3255
  return null;
3256
+ // A loop that runs a pending test on every iteration observes a
3257
+ // terminate request through it; a separate poll would only add work.
3258
+ if (loopTestsEveryIteration(loop, (e) => this.callTestsPending(e)))
3259
+ return null;
3043
3260
  const slot = this.B.slot();
3044
3261
  this.B.entryAllocas.push(`${slot} = alloca i32 ; worker cancellation budget`);
3045
3262
  this.B.line(`store i32 0, ptr ${slot}`);
@@ -3061,7 +3278,7 @@ export class LlEmitter {
3061
3278
  // Calls and throwing operations retain their own checks. Amortize the
3062
3279
  // extra cancellation poll across bounded loop batches so scalar work
3063
3280
  // does not cross the runtime ABI on every iteration.
3064
- this.emitPendingCheck();
3281
+ this.emitPendingCheck(undefined, true);
3065
3282
  B.br(next);
3066
3283
  B.startBlock(next);
3067
3284
  }
@@ -3665,18 +3882,38 @@ export class LlEmitter {
3665
3882
  this.declare(`declare void @scr_box_set_ref(ptr, ptr)`);
3666
3883
  B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${cell})`);
3667
3884
  }
3885
+ /** The payload a TDZ box holds before its declaration runs, when that is
3886
+ * not NULL. A nullable union's value is its arm pointer with NULL as a
3887
+ * unit arm (nullable-unions.ts), so a stored `null`/`undefined` would
3888
+ * read back as the temporal dead zone; such boxes start at the module's
3889
+ * immortal NULLABLE_ABSENT sentinel instead, which no value of the
3890
+ * binding can hold and every RC entry point and the collector skip. */
3891
+ tdzEmptySentinel(t) {
3892
+ if (!this.nullableUnions.of(t))
3893
+ return null;
3894
+ this.needsNullableAbsent = true;
3895
+ this.immortalValues.add(NULLABLE_ABSENT);
3896
+ return NULLABLE_ABSENT;
3897
+ }
3668
3898
  /** The TDZ-guarded read of a boxed binding: an empty payload slot is
3669
3899
  * the temporal dead zone — throw Node's exact catchable ReferenceError
3670
3900
  * (exprs.ts's varRef guard). Scalars then peek the one-element
3671
3901
  * array cell; ref kinds read the box normally (+1). */
3672
- checkTdz(box, name) {
3902
+ checkTdz(box, t, name) {
3673
3903
  const B = this.B;
3674
3904
  const slotp = B.tmp();
3675
3905
  const slotv = B.tmp();
3676
3906
  const empty = B.tmp();
3677
3907
  B.line(`${slotp} = getelementptr inbounds %ScrBox, ptr ${box}, i64 0, i32 5`);
3678
- B.line(`${slotv} = load i64, ptr ${slotp}`);
3679
- B.line(`${empty} = icmp eq i64 ${slotv}, 0`);
3908
+ const sentinel = this.tdzEmptySentinel(t);
3909
+ if (sentinel !== null) {
3910
+ B.line(`${slotv} = load ptr, ptr ${slotp}`);
3911
+ B.line(`${empty} = icmp eq ptr ${slotv}, ${sentinel}`);
3912
+ }
3913
+ else {
3914
+ B.line(`${slotv} = load i64, ptr ${slotp}`);
3915
+ B.line(`${empty} = icmp eq i64 ${slotv}, 0`);
3916
+ }
3680
3917
  this.throwIfUninitialized(empty, name);
3681
3918
  return slotv;
3682
3919
  }
@@ -3713,7 +3950,7 @@ export class LlEmitter {
3713
3950
  B.startBlock(lk);
3714
3951
  }
3715
3952
  tdzBoxRead(box, t, name) {
3716
- const slotv = this.checkTdz(box, name);
3953
+ const slotv = this.checkTdz(box, t, name);
3717
3954
  const B = this.B;
3718
3955
  const acc = boxAccess(t);
3719
3956
  if (acc === "ref")
@@ -3733,7 +3970,7 @@ export class LlEmitter {
3733
3970
  writeBindingBox(box, local, value, initializes = false, borrowed = false) {
3734
3971
  if (local.tdz) {
3735
3972
  const first = initializes || !local.mutable;
3736
- const slotv = first ? null : this.checkTdz(box, local.name);
3973
+ const slotv = first ? null : this.checkTdz(box, local.type, local.name);
3737
3974
  const acc = boxAccess(local.type);
3738
3975
  if (acc !== "ref") {
3739
3976
  if (first)
@@ -3893,6 +4130,7 @@ export class LlEmitter {
3893
4130
  const numericFn = withInitializerBindings(fn, initializerBindings);
3894
4131
  this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
3895
4132
  this.borrowedParameters.clear();
4133
+ this.referenceWrites?.enter(fn);
3896
4134
  this.reboundOwnerSlots.clear();
3897
4135
  this.projectedParameters.clear();
3898
4136
  const projectedParameterIndexes = this.callLifetimes.parameters.get(fn.name);
@@ -3912,6 +4150,8 @@ export class LlEmitter {
3912
4150
  this.captureIds = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId));
3913
4151
  this.integerLoopBindings.clear();
3914
4152
  this.countedLoopsEnabled = this.debug === null && !fn.async && !fn.generator;
4153
+ this.finalFieldBorrows =
4154
+ !fn.async && !fn.generator && constructorClass(fn.name) === null && this.finalFields.size > 0;
3915
4155
  this.byteWindowsEnabled = this.countedLoopsEnabled;
3916
4156
  this.byteWindowEntries =
3917
4157
  this.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
@@ -3922,10 +4162,10 @@ export class LlEmitter {
3922
4162
  this.integerArrayBindings.clear();
3923
4163
  this.loopArrayBorrows =
3924
4164
  this.debug === null
3925
- ? findLoopArrayBorrows(fn, this.callLifetimes, this.referenceEffects, this.optionalArrayReads)
4165
+ ? findLoopArrayBorrows(fn, this.callLifetimes, this.referenceEffects, this.optionalArrayReads, this.referenceWrites)
3926
4166
  : new Set();
3927
- this.localArrayReads = findLocalArrayReads(fn, this.fnByName, this.unionsById, this.referenceEffects.functions, this.callLifetimes, this.optionalArrayReads, this.loopArrayBorrows);
3928
- this.callArrayReads = findCallArrayReads(fn, this.optionalArrayReads, this.referenceEffects.functions, this.callLifetimes);
4167
+ this.localArrayReads = findLocalArrayReads(fn, this.fnByName, this.unionsById, this.referenceEffects.functions, this.callLifetimes, this.optionalArrayReads, this.loopArrayBorrows, this.debug === null ? this.referenceEffects : undefined);
4168
+ this.callArrayReads = findCallArrayReads(fn, this.optionalArrayReads, this.referenceEffects.functions, this.callLifetimes, this.debug === null ? this.referenceEffects : undefined);
3929
4169
  this.scalarStringSlices =
3930
4170
  this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
3931
4171
  this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
@@ -3938,9 +4178,11 @@ export class LlEmitter {
3938
4178
  this.chainSlots.clear();
3939
4179
  this.finallyStack = [];
3940
4180
  this.tryStack = [];
4181
+ this.loopDepth = 0;
3941
4182
  this.currentReturnType = fn.returnType;
3942
4183
  this.currentGenerator = fn.generator ?? null;
3943
4184
  this.currentBorrowedReturn = this.borrowedReturns.has(fn.name);
4185
+ this.inConstructor = this.constructorFunctions.has(fn.name);
3944
4186
  this.currentWasiCoro = null;
3945
4187
  this.logArgSlots = 0;
3946
4188
  if (this.wasi && (fn.async === true || fn.generator !== undefined)) {
@@ -4100,8 +4342,8 @@ export class LlEmitter {
4100
4342
  if (this.stackChecks?.has(fn.name))
4101
4343
  this.emitStackCheck();
4102
4344
  if (this.workerEntryPolls?.has(fn.name))
4103
- this.emitPendingCheck();
4104
- this.emitStmts(fn.body);
4345
+ this.emitPendingCheck(undefined, true);
4346
+ this.emitStmts(fn.body, true);
4105
4347
  // Implicit exit of a void function: release the function scope unless
4106
4348
  // the body already terminated its final block (return, or a throw
4107
4349
  // whose unwind released everything down to depth 0).
@@ -4167,7 +4409,14 @@ export class LlEmitter {
4167
4409
  if (fn.captures !== undefined)
4168
4410
  params.unshift("ptr %sc_env");
4169
4411
  const ret = this.llType(fn.returnType);
4170
- const attrs = coro !== null ? "#1" : FN_ATTRS;
4412
+ let attrs = coro !== null ? "#1" : FN_ATTRS;
4413
+ // The shared indexed-read helper (lowerSafeIndexRead) is a dense bounds
4414
+ // test, a state test and a load behind cold runtime fallbacks, called
4415
+ // from every `a[i]` of one element type. Without the hint LLVM keeps the
4416
+ // hot instances out of line once they have many callers: every read pays
4417
+ // a call, and an integer index converts to a double and back across it.
4418
+ if (fn.name.startsWith("%arr.idxOr."))
4419
+ attrs = `inlinehint ${attrs}`;
4171
4420
  const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`;
4172
4421
  const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
4173
4422
  const body = `define internal ${ret} @${symbol}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
@@ -4250,14 +4499,34 @@ export class LlEmitter {
4250
4499
  return this.canBorrowCallArgument(value.value);
4251
4500
  if (value.kind === "libCall")
4252
4501
  return value.fn === "error.nodeThrow";
4502
+ // A final field never changes outside constructors: while its object is
4503
+ // borrowable, so is the field's value. Nullable-pointer fields have no
4504
+ // box to lend and keep their owning path.
4505
+ if (value.kind === "fieldGet" &&
4506
+ this.finalFieldBorrows &&
4507
+ isRefCounted(value.type) &&
4508
+ this.finalFields.isFinal(value.className, value.field) &&
4509
+ !this.nullableFieldGet(value))
4510
+ return this.canBorrowReceiver(value.obj) && this.canBorrowCallArgument(value.obj);
4253
4511
  if (value.kind === "ternary")
4254
4512
  return (this.canBorrowReceiver(value.then) &&
4255
4513
  this.canBorrowReceiver(value.else_) &&
4256
4514
  this.canBorrowCallArgument(value.then) &&
4257
4515
  this.canBorrowCallArgument(value.else_));
4516
+ // A construction-only field of a stable owner keeps its value until the
4517
+ // owner dies: no later operand or callee can replace and release it.
4518
+ if ((value.kind === "fieldGet" || value.kind === "recordGet") && this.immutableRead(value))
4519
+ return this.canBorrowCallArgument(value.obj);
4520
+ // An optional chain's bound receiver stays unchanged until its body
4521
+ // finishes (see emitReadReceiver).
4522
+ if (value.kind === "chainRecv")
4523
+ return this.chainSlots.has(value.id);
4258
4524
  if (value.kind !== "varRef")
4259
4525
  return false;
4260
4526
  const binding = this.binding(value.localId);
4527
+ // A module constant owns its value for the rest of the program.
4528
+ if (binding.kind === "global")
4529
+ return this.stableGlobals.has(value.localId);
4261
4530
  return (binding.kind === "local" &&
4262
4531
  binding.local !== undefined &&
4263
4532
  (!binding.local.mutable ||
@@ -4267,15 +4536,212 @@ export class LlEmitter {
4267
4536
  !binding.local.boxed &&
4268
4537
  !binding.local.tdz);
4269
4538
  }
4539
+ /** A plain-pointer read of a construction-only field outside any
4540
+ * constructor (ImmutableFields). */
4541
+ immutableRead(e) {
4542
+ if (this.inConstructor || this.immutableFields === null)
4543
+ return false;
4544
+ const t = e.type.kind;
4545
+ if (t !== "object" && t !== "record" && t !== "array" && t !== "string")
4546
+ return false;
4547
+ return e.kind === "fieldGet"
4548
+ ? this.immutableFields.classField(e.className, e.field)
4549
+ : this.immutableFields.recordField(e.shapeId, e.field);
4550
+ }
4551
+ /** A projection read whose locations nothing writes while a borrowed
4552
+ * read of it is in use (ReferenceWrites), given the writes of that
4553
+ * extent. The root binding itself must stay alive across it too, which
4554
+ * its own location (`l:`, `g:`, `bx`) covers. */
4555
+ unwrittenRead(e, writes) {
4556
+ const rw = this.referenceWrites;
4557
+ if (rw === null)
4558
+ return false;
4559
+ const keys = rw.readKeys(e, (call) => this.borrowedReturnKeys(call.callee));
4560
+ return keys !== null && !rw.conflicts(keys, writes);
4561
+ }
4562
+ /** A stable binding initialized by a projection can borrow it when the
4563
+ * rest of its block (its whole lifetime) writes none of the projection's
4564
+ * locations: the source edge then keeps the value alive, and uses that
4565
+ * escape still retain their own copies. */
4566
+ unwrittenAlias(s) {
4567
+ const cursor = this.stmtCursor;
4568
+ const rw = this.referenceWrites;
4569
+ if (rw === null || s.init === null || cursor === null || cursor.stmts[cursor.index] !== s)
4570
+ return false;
4571
+ if (!this.plainReference(s.init.type) ||
4572
+ !this.canBorrowReceiver(s.init) ||
4573
+ !this.unownedWalk(s.init))
4574
+ return false;
4575
+ const writes = rw.union([rw.expr(s.init), rw.stmts(cursor.stmts.slice(cursor.index + 1))]);
4576
+ return this.unwrittenRead(s.init, writes);
4577
+ }
4578
+ /** An optional element read into a projection-only local can borrow the
4579
+ * element when the rest of the block writes no array element and none of
4580
+ * the locations keeping the array alive (which must itself be read
4581
+ * without a frame owner). */
4582
+ unwrittenLocalArrayRead(s, read) {
4583
+ const cursor = this.stmtCursor;
4584
+ const rw = this.referenceWrites;
4585
+ if (rw === null || s.init === null || cursor === null || cursor.stmts[cursor.index] !== s)
4586
+ return false;
4587
+ if (!this.unownedWalk(read.array))
4588
+ return false;
4589
+ const keys = rw.readKeys(read.array);
4590
+ if (keys === null || rw.expr(read.index) !== null)
4591
+ return false;
4592
+ const writes = rw.union([rw.expr(s.init), rw.stmts(cursor.stmts.slice(cursor.index + 1))]);
4593
+ return !rw.conflicts(["a", ...keys], writes);
4594
+ }
4595
+ /** An optional element read passed to a projection-only parameter can
4596
+ * borrow the element when the callee and later arguments (`writes`)
4597
+ * write no array element and nothing keeping the array alive. */
4598
+ unwrittenCallArrayRead(read, writes) {
4599
+ const rw = this.referenceWrites;
4600
+ if (rw === null)
4601
+ return false;
4602
+ const keys = rw.readKeys(read.array);
4603
+ if (keys === null || rw.expr(read.index) !== null)
4604
+ return false;
4605
+ return !rw.conflicts(["a", ...keys], writes);
4606
+ }
4607
+ /** `return x` of a local whose slot is an owned scope entry of the same
4608
+ * type: the return can take over the local's reference. */
4609
+ movableReturn(value) {
4610
+ if (value === null || value.kind !== "varRef" || !isRefCounted(value.type))
4611
+ return null;
4612
+ if (this.currentBorrowedReturn || this.currentWasiCoro !== null)
4613
+ return null;
4614
+ const binding = this.binding(value.localId);
4615
+ if (binding.kind !== "local" || binding.local === undefined || binding.local.boxed)
4616
+ return null;
4617
+ if (!typeEquals(binding.type, value.type) || !typeEquals(this.currentReturnType, value.type))
4618
+ return null;
4619
+ for (const scope of this.scopes) {
4620
+ const index = scope.findIndex((entry) => entry.slot === binding.slot);
4621
+ if (index < 0)
4622
+ continue;
4623
+ const entry = scope[index];
4624
+ if (entry.boxed || entry.lazy || !typeEquals(entry.type, value.type))
4625
+ return null;
4626
+ return { scope, entry, index };
4627
+ }
4628
+ return null;
4629
+ }
4630
+ /** A walk host for a body that may write the heap: each walk must read
4631
+ * only locations the whole body (with its callees) never writes;
4632
+ * rebinding the walk locals themselves releases nothing. */
4633
+ unwrittenWalkHost(fn, host) {
4634
+ return {
4635
+ ...host,
4636
+ unwritten: (value, candidates) => this.referenceWrites?.unwrittenWalk(fn, value, candidates, (call) => this.borrowedReturnKeys(call.callee)) ?? false,
4637
+ };
4638
+ }
4639
+ /** emitReadReceiver yields a plain pointer for `e` that no statement
4640
+ * frame owns, so it can outlive the statement (walk-borrows' walks, from
4641
+ * any unboxed plain-pointer binding, plus element reads). */
4642
+ unownedWalk(e) {
4643
+ switch (e.kind) {
4644
+ case "varRef": {
4645
+ const binding = this.binding(e.localId);
4646
+ if (binding.kind === "global")
4647
+ return this.plainReference(binding.type);
4648
+ return (binding.kind === "local" &&
4649
+ binding.local !== undefined &&
4650
+ !binding.local.boxed &&
4651
+ !binding.local.tdz &&
4652
+ this.plainReference(binding.local.type) &&
4653
+ !this.stringSlices.has(e.localId) &&
4654
+ !this.storedSplits.has(e.localId) &&
4655
+ !this.localUnionStorage.has(e.localId));
4656
+ }
4657
+ case "fieldGet":
4658
+ case "recordGet":
4659
+ return this.borrowsWithoutOwning(e) && this.unownedWalk(e.obj);
4660
+ case "unionNarrow":
4661
+ case "downcast":
4662
+ case "upcast":
4663
+ return this.borrowsWithoutOwning(e) && this.unownedWalk(e.value);
4664
+ case "unionWrap":
4665
+ if (!this.borrowsWithoutOwning(e))
4666
+ return false;
4667
+ return e.value.kind === "unitLit" || this.unownedWalk(e.value);
4668
+ case "ternary": {
4669
+ const arm = (v) => (v.kind === "libCall" && v.fn === "error.nodeThrow") || this.unownedWalk(v);
4670
+ return this.borrowsWithoutOwning(e) && arm(e.then) && arm(e.else_);
4671
+ }
4672
+ case "call":
4673
+ return (this.borrowsWithoutOwning(e) &&
4674
+ e.args.every((arg) => !isRefCounted(arg.type) || this.unownedWalk(arg)));
4675
+ case "arrayGet":
4676
+ return (!this.isStackUnionSource(e) &&
4677
+ this.optionalArrayReads.get(e) === null &&
4678
+ this.unownedWalk(e.arr));
4679
+ case "seqExpr": {
4680
+ const guarded = guardedValue(e);
4681
+ return guarded !== null && this.unownedWalk(guarded);
4682
+ }
4683
+ default:
4684
+ return false;
4685
+ }
4686
+ }
4687
+ /** Every location a borrowed-return callee's walks can read (its body's
4688
+ * field, record and element reads, through nested borrowed returns). */
4689
+ borrowedReturnKeys(callee) {
4690
+ if (!this.borrowedReturns.has(callee))
4691
+ return null;
4692
+ const known = this.borrowedReturnKeyCache.get(callee);
4693
+ if (known)
4694
+ return known;
4695
+ const rw = this.referenceWrites;
4696
+ const keys = new Set();
4697
+ const seen = new Set([callee]);
4698
+ const pending = [callee];
4699
+ while (pending.length > 0) {
4700
+ const fn = this.fnByName.get(pending.pop());
4701
+ if (!fn)
4702
+ continue;
4703
+ everyStmtList(fn.body, {
4704
+ stmt: () => true,
4705
+ expr: (e) => {
4706
+ if (e.kind === "fieldGet")
4707
+ keys.add(rw.fieldKey(e.className, e.field));
4708
+ else if (e.kind === "recordGet") {
4709
+ keys.add(`r:${e.field}`);
4710
+ keys.add("r:*");
4711
+ }
4712
+ else if (e.kind === "arrayGet")
4713
+ keys.add("a");
4714
+ else if (e.kind === "call" && this.borrowedReturns.has(e.callee) && !seen.has(e.callee)) {
4715
+ seen.add(e.callee);
4716
+ pending.push(e.callee);
4717
+ }
4718
+ return true;
4719
+ },
4720
+ });
4721
+ }
4722
+ const result = [...keys];
4723
+ this.borrowedReturnKeyCache.set(callee, result);
4724
+ return result;
4725
+ }
4270
4726
  // ── statements ──────────────────────────────────────────────────────────
4271
- emitStmts(stmts) {
4272
- for (const s of stmts) {
4273
- // Statements after a terminator are unreachable (dead code after
4274
- // return/break/continue). Skip them so no dropped SSA definition
4275
- // can leak forward.
4276
- if (this.B.isTerminated())
4277
- return;
4278
- this.emitStmt(s);
4727
+ emitStmts(stmts, scoped = false) {
4728
+ const saved = this.stmtCursor;
4729
+ const cursor = scoped ? { stmts, index: 0 } : null;
4730
+ try {
4731
+ for (let i = 0; i < stmts.length; i++) {
4732
+ // Statements after a terminator are unreachable (dead code after
4733
+ // return/break/continue). Skip them so no dropped SSA definition
4734
+ // can leak forward.
4735
+ if (this.B.isTerminated())
4736
+ return;
4737
+ if (cursor)
4738
+ cursor.index = i;
4739
+ this.stmtCursor = cursor;
4740
+ this.emitStmt(stmts[i]);
4741
+ }
4742
+ }
4743
+ finally {
4744
+ this.stmtCursor = saved;
4279
4745
  }
4280
4746
  }
4281
4747
  /** Emits a block in its own lexical scope (refcounted locals released at
@@ -4287,7 +4753,7 @@ export class LlEmitter {
4287
4753
  const scope = [];
4288
4754
  this.scopes.push(scope);
4289
4755
  setup?.(scope);
4290
- this.emitStmts(stmts);
4756
+ this.emitStmts(stmts, true);
4291
4757
  const ended = endsWithJump(stmts);
4292
4758
  this.scopes.pop();
4293
4759
  if (!ended)
@@ -4296,11 +4762,16 @@ export class LlEmitter {
4296
4762
  emitStmt(s) {
4297
4763
  const previous = this.B.debugLocation;
4298
4764
  this.B.debugLocation = this.debug?.location(s.loc, this.debugScope) ?? null;
4765
+ const loop = s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf";
4766
+ if (loop)
4767
+ this.loopDepth++;
4299
4768
  try {
4300
4769
  this.emitStmtBody(s);
4301
4770
  }
4302
4771
  finally {
4303
4772
  this.B.debugLocation = previous;
4773
+ if (loop)
4774
+ this.loopDepth--;
4304
4775
  }
4305
4776
  }
4306
4777
  emitStmtBody(s) {
@@ -4344,7 +4815,9 @@ export class LlEmitter {
4344
4815
  }
4345
4816
  const localRead = this.localArrayReads.get(s.localId);
4346
4817
  if (localRead) {
4347
- const owner = emitLocalArrayRead(this, localRead, b.slot);
4818
+ const owner = emitLocalArrayRead(this, !localRead.borrow && this.unwrittenLocalArrayRead(s, localRead)
4819
+ ? { ...localRead, borrow: true }
4820
+ : localRead, b.slot);
4348
4821
  if (owner)
4349
4822
  this.scopes[this.scopes.length - 1].push(owner);
4350
4823
  break;
@@ -4397,8 +4870,14 @@ export class LlEmitter {
4397
4870
  B.line(`${box} = ${boxNew} ; let ${b.local.name} (boxed)`);
4398
4871
  B.line(`store ptr ${box}, ptr ${b.slot}`);
4399
4872
  this.scopes[this.scopes.length - 1].push({ slot: b.slot, type: b.type, boxed: true });
4400
- if (s.init === null)
4873
+ if (s.init === null) {
4874
+ // An empty TDZ box whose payload can legitimately be NULL
4875
+ // starts at its distinct sentinel (tdzEmptySentinel).
4876
+ const sentinel = b.local.tdz === true ? this.tdzEmptySentinel(b.type) : null;
4877
+ if (sentinel !== null)
4878
+ B.line(`store ptr ${sentinel}, ptr ${this.boxSlot(box)}`);
4401
4879
  break;
4880
+ }
4402
4881
  const v = this.emitExpr(s.init);
4403
4882
  if (isRefCounted(v.type))
4404
4883
  this.moveTemp(v); // the box takes ownership
@@ -4432,7 +4911,7 @@ export class LlEmitter {
4432
4911
  b.local &&
4433
4912
  isRefCounted(b.type) &&
4434
4913
  this.stableCallBindings.has(s.localId) &&
4435
- this.canBorrowCallArgument(s.init)) {
4914
+ (this.canBorrowCallArgument(s.init) || this.unwrittenAlias(s))) {
4436
4915
  const v = this.emitReadReceiver(s.init);
4437
4916
  B.line(`store ptr ${v.name}, ptr ${b.slot}`);
4438
4917
  break;
@@ -4499,6 +4978,32 @@ export class LlEmitter {
4499
4978
  }
4500
4979
  B.line(`store ${this.llType(b.type)} ${v.name}, ptr ${b.slot}`);
4501
4980
  this.storeIntegerView(s.localId, v, s.value);
4981
+ if (b.kind === "global" && this.immortalGlobals.has(s.localId)) {
4982
+ // An Immutable module constant (contracts/check.ts proved it is
4983
+ // fresh, publishable and never written): its graph becomes
4984
+ // immortal, so reads skip reference counting and the collector
4985
+ // never visits it.
4986
+ const lines = this.publishWalkers.emitPublish(v.name, v.type, s.loc);
4987
+ for (const line of lines)
4988
+ B.line(line.trimStart());
4989
+ // Reads of this global skip retains from here on, which is only
4990
+ // sound once the graph really is immortal. Node.js never fails
4991
+ // here, so a refusal means the contract proof was wrong: stop
4992
+ // rather than keep running with a mortal graph read as immortal.
4993
+ if (lines.length > 0) {
4994
+ this.declare(`declare zeroext i1 @scr_exc_pending()`);
4995
+ this.declare(`declare void @scr_trap(ptr)`);
4996
+ const refused = B.tmp();
4997
+ B.line(`${refused} = call zeroext i1 @scr_exc_pending()`);
4998
+ const stop = B.newLabel("immutable.refused");
4999
+ const done = B.newLabel("immutable.published");
5000
+ B.condBr(refused, stop, done);
5001
+ B.startBlock(stop);
5002
+ B.line(`call void @scr_trap(ptr ${this.cstr("scriptc: internal error: an Immutable value could not be published\n")})`);
5003
+ B.terminate("unreachable");
5004
+ B.startBlock(done);
5005
+ }
5006
+ }
4502
5007
  break;
4503
5008
  }
4504
5009
  case "exprStmt":
@@ -4545,7 +5050,7 @@ export class LlEmitter {
4545
5050
  B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
4546
5051
  this.markMemoryPointer(lenPtr, "array:header");
4547
5052
  B.line(`${len} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`);
4548
- B.line(`${lenNumber} = uitofp ${this.sizeType} ${len} to double`);
5053
+ B.line(`${lenNumber} = sitofp ${this.sizeType} ${len} to double`);
4549
5054
  B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`);
4550
5055
  B.condBr(same, done, resize);
4551
5056
  B.startBlock(resize);
@@ -4596,14 +5101,14 @@ export class LlEmitter {
4596
5101
  const obj = this.emitStableReceiver(s.obj, [s.value]);
4597
5102
  // (The guard precedes the value here: the nullable store
4598
5103
  // evaluates it itself.)
4599
- this.emitPublishedFieldGuard(obj.name, s.className, s.field);
5104
+ this.emitStoreGuard(obj.name, s);
4600
5105
  this.emitNullableFieldStore(obj.name, s.className, s.field, nullable, s.value);
4601
5106
  break;
4602
5107
  }
4603
5108
  if (s.kind === "fieldSet" && this.int32Slots.isField(s.className, s.field)) {
4604
5109
  const obj = this.emitStableReceiver(s.obj, [s.value]);
4605
5110
  const v = this.emitExpr(s.value);
4606
- this.emitPublishedFieldGuard(obj.name, s.className, s.field);
5111
+ this.emitStoreGuard(obj.name, s);
4607
5112
  const { ptr } = this.classFieldPtr(obj.name, s.className, s.field);
4608
5113
  this.storeInt32Field(ptr, v, s.value);
4609
5114
  break;
@@ -4611,7 +5116,7 @@ export class LlEmitter {
4611
5116
  const obj = this.emitStableReceiver(s.obj, [s.value]);
4612
5117
  const v = this.emitExpr(s.value);
4613
5118
  if (s.kind === "fieldSet")
4614
- this.emitPublishedFieldGuard(obj.name, s.className, s.field);
5119
+ this.emitStoreGuard(obj.name, s);
4615
5120
  else if (this.publishedTypes?.recordGuarded(s.shapeId) === true)
4616
5121
  this.emitPublishedFieldGuard(obj.name, null, s.field);
4617
5122
  const { ptr, type } = s.kind === "fieldSet"
@@ -4833,7 +5338,7 @@ export class LlEmitter {
4833
5338
  break;
4834
5339
  }
4835
5340
  case "while": {
4836
- const workerBudget = this.workerLoopBudget();
5341
+ const workerBudget = this.workerLoopBudget(s);
4837
5342
  const lc = B.newLabel("loop.c");
4838
5343
  const lb = B.newLabel("loop.b");
4839
5344
  const le = B.newLabel("loop.e");
@@ -4858,7 +5363,7 @@ export class LlEmitter {
4858
5363
  break;
4859
5364
  }
4860
5365
  case "doWhile": {
4861
- const workerBudget = this.workerLoopBudget();
5366
+ const workerBudget = this.workerLoopBudget(s);
4862
5367
  // Body first (runs at least once); continue jumps to the CONDITION.
4863
5368
  const lb = B.newLabel("loop.b");
4864
5369
  const lc = B.newLabel("loop.c");
@@ -4923,12 +5428,39 @@ export class LlEmitter {
4923
5428
  }
4924
5429
  let countedJoin = null;
4925
5430
  if (countedLoop?.guarded) {
4926
- const bound = this.emitExpr(countedLoop.limit);
4927
- const safe = B.tmp();
5431
+ let safe = "true";
5432
+ if (countedLoop.limitGuarded) {
5433
+ const bound = this.emitExpr(countedLoop.limit);
5434
+ safe = B.tmp();
5435
+ B.line(`${safe} = fcmp ${countedLoop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(countedLoop.guardLimit)}`);
5436
+ }
5437
+ if (countedLoop.startGuarded) {
5438
+ // An exact integer of at most 2^53 - 1 in magnitude, and not -0
5439
+ // (the bit patterns of the value and its round trip agree).
5440
+ // NaN and infinities fail the ordered range tests.
5441
+ const start = this.emitExpr(countedLoop.start);
5442
+ const low = B.tmp(), high = B.tmp(), raw = B.tmp(), wide = B.tmp(), back = B.tmp(), bits = B.tmp(), backBits = B.tmp(), exact = B.tmp(), inRange = B.tmp(), ok = B.tmp();
5443
+ B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`);
5444
+ B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
5445
+ B.line(`${inRange} = and i1 ${low}, ${high}`);
5446
+ B.line(`${raw} = fptosi double ${start.name} to i64`);
5447
+ B.line(`${wide} = freeze i64 ${raw}`);
5448
+ B.line(`${back} = sitofp i64 ${wide} to double`);
5449
+ B.line(`${bits} = bitcast double ${start.name} to i64`);
5450
+ B.line(`${backBits} = bitcast double ${back} to i64`);
5451
+ B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
5452
+ B.line(`${ok} = and i1 ${inRange}, ${exact}`);
5453
+ if (safe === "true")
5454
+ safe = ok;
5455
+ else {
5456
+ const both = B.tmp();
5457
+ B.line(`${both} = and i1 ${safe}, ${ok}`);
5458
+ safe = both;
5459
+ }
5460
+ }
4928
5461
  const fast = B.newLabel("counted.fast");
4929
5462
  const slow = B.newLabel("counted.slow");
4930
5463
  countedJoin = B.newLabel("counted.done");
4931
- B.line(`${safe} = fcmp ${countedLoop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(countedLoop.guardLimit)}`);
4932
5464
  B.condBr(safe, fast, slow);
4933
5465
  B.startBlock(slow);
4934
5466
  this.countedLoopsEnabled = false;
@@ -4947,7 +5479,11 @@ export class LlEmitter {
4947
5479
  if (integerArrayLoop && s.init?.kind === "varDecl" && s.init.init) {
4948
5480
  const value = this.emitExpr(s.init.init);
4949
5481
  start = B.tmp();
4950
- B.line(`${start} = fptoui double ${value.name} to ${this.sizeType}`);
5482
+ // The start is 0 or an enclosing array index plus one (at most
5483
+ // 2^32 - 1): on 64-bit targets the signed conversion is exact
5484
+ // and avoids x86-64's unsigned 64-bit conversion sequence.
5485
+ const conversion = this.sizeType === "i64" ? "fptosi" : "fptoui";
5486
+ B.line(`${start} = ${conversion} double ${value.name} to ${this.sizeType}`);
4951
5487
  this.integerArrayBindings.add(integerLoop.localId);
4952
5488
  }
4953
5489
  B.line(`store ${this.sizeType} ${start}, ptr ${integerSlot}`);
@@ -5009,7 +5545,7 @@ export class LlEmitter {
5009
5545
  };
5010
5546
  const lu = s.update || capturedLets.length > 0 ? B.newLabel("loop.u") : lc;
5011
5547
  freshenBindings();
5012
- const workerBudget = this.workerLoopBudget();
5548
+ const workerBudget = this.workerLoopBudget(s);
5013
5549
  B.br(lc);
5014
5550
  B.startBlock(lc);
5015
5551
  this.emitWorkerLoopCheck(workerBudget);
@@ -5117,7 +5653,7 @@ export class LlEmitter {
5117
5653
  const lb = B.newLabel("fof.b");
5118
5654
  const lu = B.newLabel("fof.u");
5119
5655
  const le = B.newLabel("fof.e");
5120
- const workerBudget = this.workerLoopBudget();
5656
+ const workerBudget = this.workerLoopBudget(s);
5121
5657
  B.br(lc);
5122
5658
  B.startBlock(lc);
5123
5659
  this.emitWorkerLoopCheck(workerBudget);
@@ -5136,7 +5672,7 @@ export class LlEmitter {
5136
5672
  B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
5137
5673
  this.markMemoryPointer(lenPtr, "array:header");
5138
5674
  B.line(`${rawLen} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`);
5139
- B.line(`${len} = uitofp ${this.sizeType} ${rawLen} to double`);
5675
+ B.line(`${len} = sitofp ${this.sizeType} ${rawLen} to double`);
5140
5676
  B.line(`${inBounds} = fcmp olt double ${i}, ${len}`);
5141
5677
  cur = i;
5142
5678
  }
@@ -5253,10 +5789,32 @@ export class LlEmitter {
5253
5789
  // the actual ret. The parked value owns a synthetic slot-backed
5254
5790
  // scope entry during each copy so a throwing finally releases it.
5255
5791
  let v = null;
5256
- if (s.value !== null && this.currentBorrowedReturn) {
5792
+ const moved = this.finallyStack.length === 0 ? this.movableReturn(s.value) : null;
5793
+ if (moved !== null) {
5794
+ // Returning an owned local hands its reference to the caller: no
5795
+ // retain now and no release of the local below.
5796
+ const t = B.tmp();
5797
+ B.line(`${t} = load ptr, ptr ${moved.entry.slot}`);
5798
+ v = { name: t, type: s.value.type };
5799
+ moved.scope.splice(moved.scope.indexOf(moved.entry), 1);
5800
+ this.releaseForJump(0, 0);
5801
+ moved.scope.splice(moved.index, 0, moved.entry);
5802
+ }
5803
+ else if (s.value !== null && this.currentBorrowedReturn) {
5257
5804
  // 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);
5805
+ // (it has no try statements, so no finally can intervene). An
5806
+ // optional element read lends the element its array keeps alive.
5807
+ const read = this.optionalArrayReads.get(s.value);
5808
+ if (read) {
5809
+ const slot = B.slot();
5810
+ B.entryAllocas.push(`${slot} = alloca ptr`);
5811
+ emitLocalArrayRead(this, { ...read, borrow: true }, slot);
5812
+ const t = B.tmp();
5813
+ B.line(`${t} = load ptr, ptr ${slot}`);
5814
+ v = { name: t, type: s.value.type };
5815
+ }
5816
+ else
5817
+ v = this.emitReadReceiver(s.value);
5260
5818
  }
5261
5819
  else if (s.value !== null) {
5262
5820
  v = this.emitExpr(s.value);
@@ -5293,7 +5851,7 @@ export class LlEmitter {
5293
5851
  this.finallyStack = savedFinally;
5294
5852
  this.tryStack = savedTry;
5295
5853
  }
5296
- else {
5854
+ else if (moved === null) {
5297
5855
  this.releaseForJump(0, 0);
5298
5856
  }
5299
5857
  if (!B.isTerminated()) {
@@ -5551,7 +6109,7 @@ export class LlEmitter {
5551
6109
  const caseLabels = s.cases.map(() => B.newLabel("sw.c"));
5552
6110
  const defaultIdx = s.cases.findIndex((c) => c.test === null);
5553
6111
  const fallback = defaultIdx >= 0 ? caseLabels[defaultIdx] : end;
5554
- if (!emitLiteralSwitch(this, disc, s.cases, caseLabels, fallback)) {
6112
+ if (!emitLiteralSwitch(this, disc, s.disc, s.cases, caseLabels, fallback)) {
5555
6113
  s.cases.forEach((c, i) => {
5556
6114
  if (c.test === null)
5557
6115
  return;
@@ -5887,6 +6445,13 @@ export class LlEmitter {
5887
6445
  ``,
5888
6446
  ];
5889
6447
  }
6448
+ /** The frozen-object guard of a class field store, unless the store
6449
+ * provably targets a still-private `this` (PublishedTypes.privateStores). */
6450
+ emitStoreGuard(obj, s) {
6451
+ if (this.publishedTypes?.privateStores.has(s) === true)
6452
+ return;
6453
+ this.emitPublishedFieldGuard(obj, s.className, s.field);
6454
+ }
5890
6455
  /** The frozen-object guard of a class field or record field store:
5891
6456
  * Node's "Cannot assign to read only property" TypeError when the target
5892
6457
  * is published. `className` null means a record (`#<Object>`). */
@@ -5994,6 +6559,16 @@ export class LlEmitter {
5994
6559
  this.B.line(`${value} = load ptr, ptr ${binding.slot}`);
5995
6560
  return { name: value, type: e.type };
5996
6561
  }
6562
+ if (e.kind === "chainRecv") {
6563
+ // The chain's bind slot holds its receiver unchanged until the body
6564
+ // finishes, owned by the slot itself or by the chain's receiver.
6565
+ const bound = this.chainSlots.get(e.id);
6566
+ if (!bound)
6567
+ throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`);
6568
+ const value = this.B.tmp();
6569
+ this.B.line(`${value} = load ${this.llType(bound.type)}, ptr ${bound.name}`);
6570
+ return { name: value, type: e.type };
6571
+ }
5997
6572
  if (e.kind === "arrayGet" && this.canBorrowReceiver(e)) {
5998
6573
  // The array keeps the element alive until the consumer; a missing
5999
6574
  // element still traps exactly like the owned read.
@@ -6016,6 +6591,10 @@ export class LlEmitter {
6016
6591
  this.ownSlot(result.owner.slot, result.owner.type);
6017
6592
  return result.value;
6018
6593
  }
6594
+ // A borrowed-return helper (such as an optional element read) lends its
6595
+ // result like any borrowed-return call.
6596
+ if (e.kind === "call" && this.borrowedReturns.has(e.callee) && this.canBorrowReceiver(e))
6597
+ return emitBorrowedResultCall(this, e);
6019
6598
  const arrayRead = this.optionalArrayReads.get(e);
6020
6599
  if (arrayRead)
6021
6600
  return emitProjectedArrayRead(this, arrayRead);
@@ -6229,6 +6808,8 @@ export class LlEmitter {
6229
6808
  }
6230
6809
  canBorrowReceiver(e) {
6231
6810
  switch (e.kind) {
6811
+ case "chainRecv":
6812
+ return this.chainSlots.has(e.id);
6232
6813
  case "varRef": {
6233
6814
  const binding = this.binding(e.localId);
6234
6815
  return (binding.kind !== "boxed" ||
@@ -6259,12 +6840,24 @@ export class LlEmitter {
6259
6840
  const wrap = this.borrowableNullableWrap(e);
6260
6841
  return wrap === "unit" || (wrap === "ref" && this.canBorrowReceiver(e.value));
6261
6842
  }
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)));
6843
+ // A borrowed-return result stays reachable from its borrowed
6844
+ // arguments until the consumer runs. Each argument is lent without a
6845
+ // snapshot, so neither the callee nor a later argument may replace
6846
+ // what it was read from: a callee that preserves every edge cannot,
6847
+ // and any other one must leave the argument's locations unwritten.
6848
+ case "call": {
6849
+ if (!this.borrowedReturns.has(e.callee))
6850
+ return false;
6851
+ if (!e.args.every((arg) => this.referenceEffects.preserves(arg)))
6852
+ return false;
6853
+ const preserving = this.referenceEffects.functions.has(e.callee);
6854
+ if (preserving)
6855
+ return e.args.every((arg) => !isRefCounted(arg.type) || this.canBorrowReceiver(arg));
6856
+ if (this.referenceWrites === null)
6857
+ return false;
6858
+ const lent = borrowableInputs(this, e.args, false, this.referenceWrites.summary(e.callee));
6859
+ return e.args.every((arg, i) => !isRefCounted(arg.type) || lent[i] === true);
6860
+ }
6268
6861
  // A required element read: the (borrowed) array owns the element
6269
6862
  // while an edge-preserving index computes.
6270
6863
  case "arrayGet":
@@ -6310,14 +6903,17 @@ export class LlEmitter {
6310
6903
  };
6311
6904
  const hostFor = (fn) => {
6312
6905
  const allowed = borrowedIds(fn);
6313
- return {
6906
+ const host = {
6314
6907
  pointerLocal: (local) => pointer(local.type),
6315
6908
  borrowsWithoutOwning: (e) => this.borrowsWithoutOwning(e),
6316
6909
  borrowedReturn: (callee) => this.borrowedReturns.has(callee),
6317
6910
  parameterAllowed: (id) => allowed.has(id),
6318
6911
  };
6912
+ return this.referenceEffects.functions.has(fn.name) ? host : this.unwrittenWalkHost(fn, host);
6319
6913
  };
6320
- const preserving = [...this.fnByName.values()].filter((fn) => this.referenceEffects.functions.has(fn.name));
6914
+ // With ReferenceWrites, a body that writes none of its walks' locations
6915
+ // qualifies like one that preserves every edge.
6916
+ const preserving = [...this.fnByName.values()].filter((fn) => this.referenceEffects.functions.has(fn.name) || this.referenceWrites !== null);
6321
6917
  const candidates = preserving.filter((fn) => {
6322
6918
  if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures)
6323
6919
  return false;
@@ -6338,6 +6934,8 @@ export class LlEmitter {
6338
6934
  if (!analyzeWalks(fn, hostFor(fn)).returnsWalk)
6339
6935
  continue;
6340
6936
  this.borrowedReturns.add(fn.name);
6937
+ // Nested borrowed returns extend the locations a caller depends on.
6938
+ this.borrowedReturnKeyCache.clear();
6341
6939
  changed = true;
6342
6940
  }
6343
6941
  }
@@ -6365,6 +6963,12 @@ export class LlEmitter {
6365
6963
  return this.nullableFieldGet(e) === null;
6366
6964
  case "recordGet":
6367
6965
  return true;
6966
+ // emitReadReceiver lends a required element of a borrowed array when
6967
+ // computing the index removes no edge.
6968
+ case "arrayGet":
6969
+ return (e.arr.type.kind === "array" &&
6970
+ this.optionalArrayReads.get(e) === null &&
6971
+ this.referenceEffects.preserves(e.index));
6368
6972
  case "unionNarrow":
6369
6973
  return this.nullableUnions.has(e.unionId);
6370
6974
  case "downcast":