@scriptc/compiler 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +1 -0
  2. package/dist/backend/cycle-analysis.js +50 -17
  3. package/dist/backend/cycle-analysis.js.map +1 -1
  4. package/dist/backend/cycle-analysis.test.js +138 -14
  5. package/dist/backend/cycle-analysis.test.js.map +1 -1
  6. package/dist/backend/cycle-mutability.d.ts +31 -6
  7. package/dist/backend/cycle-mutability.js +120 -18
  8. package/dist/backend/cycle-mutability.js.map +1 -1
  9. package/dist/backend/llvm/alloc.d.ts +13 -3
  10. package/dist/backend/llvm/alloc.js +24 -10
  11. package/dist/backend/llvm/alloc.js.map +1 -1
  12. package/dist/backend/llvm/alloc.test.js +5 -5
  13. package/dist/backend/llvm/alloc.test.js.map +1 -1
  14. package/dist/backend/llvm/byte-numbers.js +2 -3
  15. package/dist/backend/llvm/byte-numbers.js.map +1 -1
  16. package/dist/backend/llvm/classes.js +7 -6
  17. package/dist/backend/llvm/classes.js.map +1 -1
  18. package/dist/backend/llvm/common.d.ts +17 -0
  19. package/dist/backend/llvm/common.js +28 -0
  20. package/dist/backend/llvm/common.js.map +1 -1
  21. package/dist/backend/llvm/dense-array-access.js +1 -1
  22. package/dist/backend/llvm/dense-array-access.test.js +1 -1
  23. package/dist/backend/llvm/dyn.d.ts +4 -4
  24. package/dist/backend/llvm/dyn.js +27 -11
  25. package/dist/backend/llvm/dyn.js.map +1 -1
  26. package/dist/backend/llvm/emitter.d.ts +45 -3
  27. package/dist/backend/llvm/emitter.js +333 -74
  28. package/dist/backend/llvm/emitter.js.map +1 -1
  29. package/dist/backend/llvm/emitter.test.js +36 -3
  30. package/dist/backend/llvm/emitter.test.js.map +1 -1
  31. package/dist/backend/llvm/expr-bytes.js +35 -28
  32. package/dist/backend/llvm/expr-bytes.js.map +1 -1
  33. package/dist/backend/llvm/expr-containers.js +1 -1
  34. package/dist/backend/llvm/expr-context.d.ts +2 -0
  35. package/dist/backend/llvm/expr-control.js +46 -5
  36. package/dist/backend/llvm/expr-control.js.map +1 -1
  37. package/dist/backend/llvm/expr-dynamic.js +3 -0
  38. package/dist/backend/llvm/expr-dynamic.js.map +1 -1
  39. package/dist/backend/llvm/expr-stream-bridges.d.ts +3 -0
  40. package/dist/backend/llvm/expr-stream-bridges.js +73 -11
  41. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  42. package/dist/backend/llvm/lib-filesystem.js +18 -2
  43. package/dist/backend/llvm/lib-filesystem.js.map +1 -1
  44. package/dist/backend/llvm/lib-shared.js +1 -0
  45. package/dist/backend/llvm/lib-shared.js.map +1 -1
  46. package/dist/backend/llvm/local-array-reads.d.ts +17 -6
  47. package/dist/backend/llvm/local-array-reads.js +155 -43
  48. package/dist/backend/llvm/local-array-reads.js.map +1 -1
  49. package/dist/backend/llvm/reference-effects.js +34 -1
  50. package/dist/backend/llvm/reference-effects.js.map +1 -1
  51. package/dist/backend/llvm/shapes.d.ts +14 -3
  52. package/dist/backend/llvm/shapes.js +43 -12
  53. package/dist/backend/llvm/shapes.js.map +1 -1
  54. package/dist/backend/llvm/shapes.test.js +5 -5
  55. package/dist/backend/llvm/shapes.test.js.map +1 -1
  56. package/dist/backend/llvm/string-reads.js +29 -26
  57. package/dist/backend/llvm/string-reads.js.map +1 -1
  58. package/dist/backend/llvm/string-slices.js +1 -1
  59. package/dist/backend/llvm/trunc.d.ts +8 -0
  60. package/dist/backend/llvm/trunc.js +28 -0
  61. package/dist/backend/llvm/trunc.js.map +1 -0
  62. package/dist/backend/loop-polls.d.ts +14 -0
  63. package/dist/backend/loop-polls.js +100 -0
  64. package/dist/backend/loop-polls.js.map +1 -0
  65. package/dist/backend/loop-polls.test.d.ts +1 -0
  66. package/dist/backend/loop-polls.test.js +86 -0
  67. package/dist/backend/loop-polls.test.js.map +1 -0
  68. package/dist/backend/may-throw.js +20 -1
  69. package/dist/backend/may-throw.js.map +1 -1
  70. package/dist/backend/may-throw.test.js +23 -0
  71. package/dist/backend/may-throw.test.js.map +1 -1
  72. package/dist/backend/native-codegen.d.ts +1 -3
  73. package/dist/backend/native-codegen.js +4 -32
  74. package/dist/backend/native-codegen.js.map +1 -1
  75. package/dist/backend/runtime-abi.d.ts +2 -2
  76. package/dist/backend/runtime-abi.js +2 -2
  77. package/dist/backend/runtime-abi.js.map +1 -1
  78. package/dist/backend/runtime-pack-targets.test.js +1 -1
  79. package/dist/backend/runtime-pack-targets.test.js.map +1 -1
  80. package/dist/backend/runtime-pack.test.js +1 -1
  81. package/dist/backend/runtime-pack.test.js.map +1 -1
  82. package/dist/cli/command.js +8 -1
  83. package/dist/cli/command.js.map +1 -1
  84. package/dist/cli/output-options.d.ts +1 -0
  85. package/dist/cli/output-options.js +3 -0
  86. package/dist/cli/output-options.js.map +1 -1
  87. package/dist/cli/usage.d.ts +5 -1
  88. package/dist/cli/usage.js +6 -0
  89. package/dist/cli/usage.js.map +1 -1
  90. package/dist/compile-types.d.ts +5 -0
  91. package/dist/executable/executable-cache.d.ts +2 -0
  92. package/dist/executable/executable-cache.js +3 -0
  93. package/dist/executable/executable-cache.js.map +1 -1
  94. package/dist/executable/executable-cache.test.js +1 -0
  95. package/dist/executable/executable-cache.test.js.map +1 -1
  96. package/dist/frontend/lowering/array-values.js +37 -1
  97. package/dist/frontend/lowering/array-values.js.map +1 -1
  98. package/dist/frontend/lowering/builtins/strings.js +4 -1
  99. package/dist/frontend/lowering/builtins/strings.js.map +1 -1
  100. package/dist/frontend/lowering/call-arguments.js +96 -4
  101. package/dist/frontend/lowering/call-arguments.js.map +1 -1
  102. package/dist/frontend/lowering/class-method-mutations.js +88 -0
  103. package/dist/frontend/lowering/class-method-mutations.js.map +1 -1
  104. package/dist/frontend/lowering/containers/bytes.js +15 -4
  105. package/dist/frontend/lowering/containers/bytes.js.map +1 -1
  106. package/dist/frontend/lowering/expressions/union-fields.d.ts +19 -0
  107. package/dist/frontend/lowering/expressions/union-fields.js +236 -0
  108. package/dist/frontend/lowering/expressions/union-fields.js.map +1 -0
  109. package/dist/frontend/lowering/lower-calls.js +12 -2
  110. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  111. package/dist/frontend/lowering/lower-exprs.d.ts +6 -0
  112. package/dist/frontend/lowering/lower-exprs.js +162 -3
  113. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  114. package/dist/frontend/lowering/lower-island.js +5 -19
  115. package/dist/frontend/lowering/lower-island.js.map +1 -1
  116. package/dist/frontend/lowering/lower-stmts.js +11 -1
  117. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  118. package/dist/frontend/lowering/lowerer.d.ts +5 -0
  119. package/dist/frontend/lowering/lowerer.js +174 -8
  120. package/dist/frontend/lowering/lowerer.js.map +1 -1
  121. package/dist/frontend/lowering/nan-coded-reads.d.ts +161 -0
  122. package/dist/frontend/lowering/nan-coded-reads.js +1342 -0
  123. package/dist/frontend/lowering/nan-coded-reads.js.map +1 -0
  124. package/dist/frontend/lowering/runtime-optional-bounds.d.ts +14 -2
  125. package/dist/frontend/lowering/runtime-optional-bounds.js +17 -11
  126. package/dist/frontend/lowering/runtime-optional-bounds.js.map +1 -1
  127. package/dist/frontend/lowering/runtime-optional-bounds.test.js +28 -0
  128. package/dist/frontend/lowering/runtime-optional-bounds.test.js.map +1 -1
  129. package/dist/frontend/lowering/surfaces.js +8 -0
  130. package/dist/frontend/lowering/surfaces.js.map +1 -1
  131. package/dist/index.d.ts +1 -1
  132. package/dist/index.js +20 -8
  133. package/dist/index.js.map +1 -1
  134. package/dist/ir/builtin-signatures.d.ts +1 -1
  135. package/dist/ir/builtin-signatures.js +2 -0
  136. package/dist/ir/builtin-signatures.js.map +1 -1
  137. package/dist/ir/constant-globals.d.ts +14 -0
  138. package/dist/ir/constant-globals.js +81 -0
  139. package/dist/ir/constant-globals.js.map +1 -0
  140. package/dist/ir/constant-globals.test.d.ts +1 -0
  141. package/dist/ir/constant-globals.test.js +97 -0
  142. package/dist/ir/constant-globals.test.js.map +1 -0
  143. package/dist/ir/final-fields.d.ts +35 -0
  144. package/dist/ir/final-fields.js +187 -0
  145. package/dist/ir/final-fields.js.map +1 -0
  146. package/dist/ir/final-fields.test.d.ts +1 -0
  147. package/dist/ir/final-fields.test.js +106 -0
  148. package/dist/ir/final-fields.test.js.map +1 -0
  149. package/dist/ir/int32-slots.d.ts +9 -0
  150. package/dist/ir/int32-slots.js +70 -57
  151. package/dist/ir/int32-slots.js.map +1 -1
  152. package/dist/ir/int32-slots.test.js +27 -0
  153. package/dist/ir/int32-slots.test.js.map +1 -1
  154. package/dist/ir/integer-loops.d.ts +5 -0
  155. package/dist/ir/integer-loops.js +20 -4
  156. package/dist/ir/integer-loops.js.map +1 -1
  157. package/dist/ir/integer-loops.test.js +17 -0
  158. package/dist/ir/integer-loops.test.js.map +1 -1
  159. package/dist/ir/ir.d.ts +6 -1
  160. package/dist/ir/ir.js +4 -0
  161. package/dist/ir/ir.js.map +1 -1
  162. package/dist/ir/serialize.d.ts +12 -2
  163. package/dist/ir/serialize.js +190 -39
  164. package/dist/ir/serialize.js.map +1 -1
  165. package/dist/ir/serialize.test.js +65 -1
  166. package/dist/ir/serialize.test.js.map +1 -1
  167. package/dist/native/cache.d.ts +8 -1
  168. package/dist/native/cache.js +38 -1
  169. package/dist/native/cache.js.map +1 -1
  170. package/dist/native/cache.test.js +11 -0
  171. package/dist/native/cache.test.js.map +1 -1
  172. package/dist/native/compiler.js +4 -3
  173. package/dist/native/compiler.js.map +1 -1
  174. package/dist/native/prepare.d.ts +3 -1
  175. package/dist/native/prepare.js +63 -28
  176. package/dist/native/prepare.js.map +1 -1
  177. package/dist/native/prepare.test.js +23 -1
  178. package/dist/native/prepare.test.js.map +1 -1
  179. package/dist/native/text-file.d.ts +10 -0
  180. package/dist/native/text-file.js +41 -0
  181. package/dist/native/text-file.js.map +1 -0
  182. package/dist/text-parts.d.ts +5 -0
  183. package/dist/text-parts.js +36 -0
  184. package/dist/text-parts.js.map +1 -0
  185. package/package.json +20 -20
  186. package/surface-manifest.json +7 -1
  187. package/dist/frontend/lowering/expressions/union-field-write.d.ts +0 -8
  188. package/dist/frontend/lowering/expressions/union-field-write.js +0 -108
  189. 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";
@@ -36,6 +37,8 @@ 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";
38
39
  import { analyzeInt32Slots } from "../../ir/int32-slots.js";
40
+ import { analyzeFinalFields, constructorClass } from "../../ir/final-fields.js";
41
+ import { foldConstantGlobals } from "../../ir/constant-globals.js";
39
42
  import { findIntegerViews } from "./integer-views.js";
40
43
  import { findInitializerBindings, withInitializerBindings } from "../../ir/initializer-bindings.js";
41
44
  import { findConstantNumericTables } from "../../ir/constant-tables.js";
@@ -64,7 +67,7 @@ import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callba
64
67
  import { findScalarStringSlices, emitStringSliceSnapshot, } from "./string-slices.js";
65
68
  import { LazyCaptures } from "./lazy-captures.js";
66
69
  import { StackCaptures } from "./stack-captures.js";
67
- import { ABSENT_FIELD_PAYLOAD, emitFieldAbsentTest, f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText, } from "./common.js";
70
+ import { ABSENT_FIELD_PAYLOAD, emitFieldAbsentTest, exactIndexLines, f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText, } from "./common.js";
68
71
  import { ffiExtendsNarrowIntegers } from "../targets.js";
69
72
  import { emitLiteralExpr, emitOperatorExpr, emitStringExpr, emitContainerExpr, emitRecordExpr, } from "./expr-primitives.js";
70
73
  import { emitControlExpr } from "./expr-control.js";
@@ -127,13 +130,13 @@ function ffiCallbackDummyLl(callback) {
127
130
  export function emitLlvmModule(mod, options = {}) {
128
131
  // Keep source storage intact for debugger inspection in dev builds.
129
132
  return new LlEmitter(options.debugSources === undefined
130
- ? scalarizeNumericRecords(specializeNumericCalls(mod, mangleFunction))
133
+ ? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
131
134
  : mod, options).emit();
132
135
  }
133
136
  /** Keep large translation units below the host's single-string limit. */
134
137
  export function emitLlvmModuleSource(mod, options = {}) {
135
138
  const parts = new LlEmitter(options.debugSources === undefined
136
- ? scalarizeNumericRecords(specializeNumericCalls(mod, mangleFunction))
139
+ ? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
137
140
  : mod, options).emitParts();
138
141
  const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1));
139
142
  if (length <= 256 * 1024 * 1024)
@@ -224,12 +227,27 @@ export class LlEmitter {
224
227
  debugScope = null;
225
228
  sizeType;
226
229
  ffiExtendNarrowIntegers;
227
- cycleColorOffset;
230
+ cycleBufferedOffset;
231
+ /** Emitted ScrCycType descriptors so far (their collector statistics
232
+ * indices; see ShapeHost.nextCycKind). */
233
+ cycKinds = 0;
228
234
  wasi;
235
+ /** The target lacks a rounding instruction at its baseline CPU (x86-64
236
+ * before SSE4.1), so `llvm.trunc` would lower to a libm call: emit
237
+ * truncation inline through an integer round trip instead. */
238
+ inlineTrunc;
229
239
  /** ELF worker executables give thread-locals the executable TLS models. */
230
240
  executableTls;
241
+ /** Source loops enclosing the statement being emitted in the current
242
+ * function body. */
243
+ loopDepth = 0;
244
+ /** Darwin worker executables read the stack guard from a pthread
245
+ * thread-specific-data slot off the thread pointer (scr_runtime.h's
246
+ * SCR_STACK_GUARD_TSD): the architecture names the access sequence. */
247
+ stackGuardTsd;
231
248
  emitLibraryIdentity;
232
249
  runtimeAbiMarker;
250
+ detachedExit;
233
251
  /** Interned string literals: UTF-8 text → { symbol, byte length } —
234
252
  * first-use order, the runtime ABI’s determinism discipline. */
235
253
  literals = new Map();
@@ -423,6 +441,11 @@ export class LlEmitter {
423
441
  /** Active induction bindings retain their width independently of size_t. */
424
442
  integerLoopBindings = new Map();
425
443
  countedLoopsEnabled = false;
444
+ /** Whole-program final reference fields (ir/final-fields.ts). */
445
+ finalFields;
446
+ /** The current function may borrow final fields of borrowable objects:
447
+ * not a constructor (where they change), not async or a generator. */
448
+ finalFieldBorrows = false;
426
449
  integerRanges = new Map();
427
450
  byteWindowsEnabled = true;
428
451
  byteWindowEntries = new Map();
@@ -472,22 +495,41 @@ export class LlEmitter {
472
495
  this.constantNumericTables = findConstantNumericTables(mod);
473
496
  this.initializerBindings = findInitializerBindings(mod);
474
497
  this.int32Slots = analyzeInt32Slots(mod);
498
+ this.finalFields = analyzeFinalFields(mod);
475
499
  this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
476
500
  this.ffiExtendNarrowIntegers =
477
501
  options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
478
502
  this.wasi = options.wasi === true;
503
+ // An empty triple targets the host, as for executableTls below: the
504
+ // Node-hosted CLI passes none, the native CLI passes its toolchain's, and
505
+ // both must emit the same module for the same target.
506
+ this.inlineTrunc =
507
+ !this.wasi &&
508
+ (options.targetTriple
509
+ ? /^(x86_64|amd64)\b/i.test(options.targetTriple)
510
+ : process.arch === "x64");
479
511
  this.executableTls =
480
512
  mod.workers === true &&
481
513
  mod.lib === undefined &&
482
514
  !this.wasi &&
483
515
  !/apple|darwin|windows|mingw|cygwin|win32/.test(options.targetTriple || process.platform);
516
+ const host = options.targetTriple || `${process.arch}-${process.platform}`;
517
+ this.stackGuardTsd =
518
+ mod.workers === true && mod.lib === undefined && !this.wasi && /apple|darwin/.test(host)
519
+ ? /^(arm64|aarch64)/.test(host)
520
+ ? "aarch64"
521
+ : /^(x86_64|amd64|x64)/.test(host)
522
+ ? "x86_64"
523
+ : null
524
+ : null;
484
525
  this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
485
526
  this.runtimeAbiMarker = options.runtimeAbiMarker === true;
527
+ this.detachedExit = options.detachedExit === true;
486
528
  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;
529
+ // ScrCycHdr.buffered sits 6 bytes behind every object. The wasm32
530
+ // header pads before its descriptor so its payload remains
531
+ // double-aligned without changing this ABI offset.
532
+ this.cycleBufferedOffset = 6;
491
533
  this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []);
492
534
  this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
493
535
  this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []);
@@ -522,7 +564,8 @@ export class LlEmitter {
522
564
  this.nullableFields = new NullableRefFields(mod.classes ?? [], this.boxedUnionsById);
523
565
  // Optional array reads also cover nullable unions: emitLocalArrayRead
524
566
  // then yields the (borrowed or owned) element pointer, not a stack box.
525
- this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById);
567
+ // Arrays of such a union are read the same way.
568
+ this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById, (id) => this.nullableUnions.has(id));
526
569
  this.checkedNarrows = new CheckedNarrows(this.fnByName);
527
570
  this.referenceEffects = new ReferenceEffects(this.fnByName, (call) => this.optionalArrayReads.get(call) !== null, mod.classes ?? []);
528
571
  const walkParameters = new Map();
@@ -578,7 +621,7 @@ export class LlEmitter {
578
621
  this.tracedUnions = traced.unions;
579
622
  this.shapeHost = {
580
623
  sizeType: this.sizeType,
581
- cycleColorOffset: this.cycleColorOffset,
624
+ cycleBufferedOffset: this.cycleBufferedOffset,
582
625
  tracedShapes: this.tracedShapes,
583
626
  tracedUnions: this.tracedUnions,
584
627
  nullableUnions: this.nullableUnions,
@@ -595,6 +638,7 @@ export class LlEmitter {
595
638
  // paths address its thread-local state.
596
639
  threadLocalAlloc: mod.workers === true,
597
640
  rcHelpers: this.rcHelpers,
641
+ nextCycKind: () => FIRST_EMITTED_CYC_KIND + this.cycKinds++,
598
642
  unionsById: this.unionsById,
599
643
  declare: (decl) => this.declare(decl),
600
644
  needOom: () => this.needOom(),
@@ -997,14 +1041,16 @@ export class LlEmitter {
997
1041
  if (!this.executableTls)
998
1042
  return parts;
999
1043
  // 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.
1044
+ // TLS block: the program's internal ones, and the runtime's external
1045
+ // ones, because the runtime is always linked statically into the
1046
+ // executable. So on ELF they can all use the local-exec model: LLVM's
1047
+ // default general-dynamic sequence (relaxed by the linker to `mov %fs:0`
1048
+ // plus an add), and even initial-exec (relaxed to `mov $offset` plus an
1049
+ // indexed access), become one %fs-relative operand LLVM can fold into
1050
+ // loads and compares. Darwin's thread-local variables have one model.
1005
1051
  return parts.map((part) => part
1006
1052
  .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 "));
1053
+ .replace(/^(@[-$._A-Za-z0-9]+ = external )thread_local global /gm, "$1thread_local(localexec) global "));
1008
1054
  }
1009
1055
  emitModuleParts() {
1010
1056
  // Function bodies first (the literal/unit/fn-value tables fill as they
@@ -1356,10 +1402,13 @@ export class LlEmitter {
1356
1402
  `%ScrClosure = type { ${this.sizeType}, ptr, ${this.sizeType}, ptr, i32, i32, ptr }`,
1357
1403
  `%ScrFfiTable = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, i8, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`,
1358
1404
  `%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 }`,
1405
+ // ScrArr mirrors scr_runtime.h field-for-field: { rc, len, cap, elem,
1406
+ // elem_retain, elem_release, elem_trace, data, present, ext }, where
1407
+ // ext points at the out-of-line sparse, property and metadata storage
1408
+ // (NULL until first used). Live dynamic stream commits swap its dense,
1409
+ // presence and out-of-line storage while preserving the target
1410
+ // object's identity.
1411
+ `%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, ptr, ptr, ptr, ptr }`,
1363
1412
  // The ScrMap prefix { rc, key_kind, val_kind, val_retain, val_release,
1364
1413
  // val_trace, key_retain, key_release, key_trace } — the inline RC
1365
1414
  // fast paths test the two trace slots (shapes.ts).
@@ -1422,6 +1471,10 @@ export class LlEmitter {
1422
1471
  const tl = this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : "";
1423
1472
  out.push(``, `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, `declare void @scr_init()`, `declare void @scr_lib_init(i32, ptr)`, ...(this.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []), ...(this.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []), ...(this.runtimeAbiMarker && this.mod.lib === undefined
1424
1473
  ? [`declare void @${RUNTIME_ABI_MARKER}()`]
1474
+ : []),
1475
+ // Overrides the runtime's weak default (scr_lib.c).
1476
+ ...(this.detachedExit && this.mod.lib === undefined
1477
+ ? [`@scr_detached_exit = constant i8 1`]
1425
1478
  : []));
1426
1479
  for (const d of this.decls)
1427
1480
  out.push(d);
@@ -1429,10 +1482,9 @@ export class LlEmitter {
1429
1482
  const flushedDecls = new Set(this.decls);
1430
1483
  for (const table of this.constantNumericTables.values()) {
1431
1484
  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`, `}`, ``);
1485
+ let tableTmp = 0;
1486
+ const tableIndex = exactIndexLines("%i", String(n), "i64", () => `%ix${tableTmp++}`);
1487
+ out.push(`@${table.symbol} = private constant [${n} x double] [${table.values.map((v) => `double ${f64Lit(v)}`).join(", ")}]`, `define internal double @${table.symbol}_get(ptr %a, double %i) alwaysinline #0 {`, `entry:`, ` %initialized = icmp ne ptr %a, null`, ...tableIndex.lines.map((line) => ` ${line}`), ` %safe = and i1 %initialized, ${tableIndex.ok}`, ` br i1 %safe, label %read, label %fallback`, `read:`, ` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, i64 0, i64 ${tableIndex.wide}`, ` %value = load double, ptr %slot`, ` ret double %value`, `fallback:`, ` %generic = call double @scr_arr_get_number(ptr %a, double %i)`, ` ret double %generic`, `}`, ``);
1436
1488
  }
1437
1489
  for (const [text, lit] of this.literals) {
1438
1490
  // Immortal interned ScrStr: { rc = SIZE_MAX, len, cap = len, bytes\0 } —
@@ -1499,7 +1551,7 @@ export class LlEmitter {
1499
1551
  // strings — a no-op.
1500
1552
  const n = inst.slots.length;
1501
1553
  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 }`);
1554
+ out.push(`@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`, `@${inst.sym}_present = internal constant [${n} x i8] ${present}`, `@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, ptr null, ptr null, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`);
1503
1555
  }
1504
1556
  if (this.templateStringsInstances.size > 0)
1505
1557
  out.push(``);
@@ -2024,7 +2076,13 @@ export class LlEmitter {
2024
2076
  // keeps the complete SameValueZero semantics.
2025
2077
  if (this.decls.has(NUMBER_MAP_ENTRY_DECL)) {
2026
2078
  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`, `}`, ``);
2079
+ let mapTmp = 0;
2080
+ const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`);
2081
+ defs.push(`define internal ptr @sc_map_entry_f64(ptr %m, double %k) ${FN_ATTRS} {`, `entry:`, ` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 19`, ` %nd = load ${sz}, ptr %ndp`,
2082
+ // A non-integral key inside the table is as absent as one outside it.
2083
+ ...mapIndex.lines.map((line) => ` ${line}`), ` br i1 ${mapIndex.ok}, label %slot, label %outside`, `slot:`, sz === "i64"
2084
+ ? ` %i = add i64 ${mapIndex.wide}, 0`
2085
+ : ` %i = trunc i64 ${mapIndex.wide} to ${sz}`, ` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 18`, ` %dense = load ptr, ptr %dp`, ` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`, ` %s = load i32, ptr %sp`, ` %empty = icmp eq i32 %s, 0`, ` br i1 %empty, label %miss, label %hit`, `hit:`, ` %e32 = sub i32 %s, 1`, sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`, ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 12`, ` %entries = load ptr, ptr %ep`, ` %found = getelementptr inbounds %ScrMapEntry, ptr %entries, ${sz} %e`, ` ret ptr %found`, `miss:`, ` ret ptr null`, `outside:`, ` %dense.any = icmp ne ${sz} %nd, 0`, ` br i1 %dense.any, label %miss, label %slow`, `slow:`, ` %r = call ptr @scr_map_entry_f64(ptr %m, double %k)`, ` ret ptr %r`, `}`, ``);
2028
2086
  }
2029
2087
  // String comparisons enter through small internal wrappers that LLVM
2030
2088
  // inlines at each site: interned literals and shared values compare by
@@ -2833,13 +2891,28 @@ export class LlEmitter {
2833
2891
  * return and stays eligible for tail-call and recursion elimination. */
2834
2892
  emitUnwind(passThrough) {
2835
2893
  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
2894
  const stop = this.B.newLabel("context.stop");
2840
2895
  const ordinary = this.B.newLabel("context.throw");
2841
- this.B.condBr(stopped, stop, ordinary);
2842
- this.B.startBlock(stop);
2896
+ if (this.executableTls) {
2897
+ // A set stop bit unwinds while the active cell may still be clear:
2898
+ // install the termination sentinel before skipping the handlers.
2899
+ const word = this.loadExcFlag();
2900
+ const bit = this.B.tmp();
2901
+ const stopped = this.B.tmp();
2902
+ this.B.line(`${bit} = and i32 ${word}, 2`);
2903
+ this.B.line(`${stopped} = icmp ne i32 ${bit}, 0`);
2904
+ this.B.condBr(stopped, stop, ordinary);
2905
+ this.B.startBlock(stop);
2906
+ this.declare(`declare zeroext i1 @scr_context_checkpoint()`);
2907
+ this.B.line(`call zeroext i1 @scr_context_checkpoint()`);
2908
+ }
2909
+ else {
2910
+ this.declare(`declare zeroext i1 @scr_context_stopping()`);
2911
+ const stopped = this.B.tmp();
2912
+ this.B.line(`${stopped} = call zeroext i1 @scr_context_stopping()`);
2913
+ this.B.condBr(stopped, stop, ordinary);
2914
+ this.B.startBlock(stop);
2915
+ }
2843
2916
  const handlers = this.tryStack;
2844
2917
  this.tryStack = [];
2845
2918
  this.emitUnwind();
@@ -2896,6 +2969,18 @@ export class LlEmitter {
2896
2969
  }
2897
2970
  this.B.br(cleanup.label);
2898
2971
  }
2972
+ /** Loads the running thread's exact pending word (ELF worker
2973
+ * executables, scr_exception.c): bit 0 mirrors the active cell, bit 1
2974
+ * requests the context's stop. It lives in the executable's static TLS
2975
+ * block, so the access is one thread-pointer-relative load. Monotonic by
2976
+ * default: a loop must not hoist the load out and miss a cross-thread
2977
+ * stop (see emitPendingCheck for the unordered tests). */
2978
+ loadExcFlag(unordered = false) {
2979
+ this.declare(`@scr_exc_flag = external thread_local(localexec) global i32, align 4`);
2980
+ const word = this.B.tmp();
2981
+ this.B.line(`${word} = load atomic i32, ptr @scr_exc_flag ${unordered ? "unordered" : "monotonic"}, align 4`);
2982
+ return word;
2983
+ }
2899
2984
  /** Instructions (two-space indented) that set the i1 `dest` to "an
2900
2985
  * exception is pending in the active context" — scr_exc_pending()'s
2901
2986
  * meaning — for the helper and adapter bodies that test it outside
@@ -2922,8 +3007,9 @@ export class LlEmitter {
2922
3007
  * call's result temp must join its frame BEFORE this runs so the unwind
2923
3008
  * releases the dummy (NULL for refcounted kinds) harmlessly. A scalar
2924
3009
  * call result of the function's return type may be passed as the unwind's
2925
- * dummy value (see emitUnwind). */
2926
- emitPendingCheck(passThrough) {
3010
+ * dummy value (see emitUnwind). `poll` marks a worker termination poll
3011
+ * (function entry or loop budget), whose test must stay ordered. */
3012
+ emitPendingCheck(passThrough, poll = false) {
2927
3013
  const through = passThrough !== undefined &&
2928
3014
  this.llType(passThrough.type) === this.llType(this.currentReturnType) &&
2929
3015
  (passThrough.type.kind === "f64" || passThrough.type.kind === "bool")
@@ -2945,16 +3031,35 @@ export class LlEmitter {
2945
3031
  return;
2946
3032
  }
2947
3033
  // 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.
3034
+ // field). ELF worker executables test the thread's exact pending word,
3035
+ // which is also nonzero once the context must stop, and unwind directly:
3036
+ // no site carries an out-of-line call, so LLVM's inliner sees the check
3037
+ // as two instructions. Other worker programs test the process-wide
3038
+ // alert word: it is nonzero while any script thread may have an
3039
+ // exception pending in its active cell or has been asked to stop, so
3040
+ // the common case is one ordinary global load (a thread-local access is
3041
+ // a call on Darwin) and the out-of-line scr_exc_pending answers for this
3042
+ // thread, observing cancellation and reinstalling the termination
3043
+ // sentinel.
2954
3044
  this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
2955
3045
  let word;
2956
- if (this.mod.workers === true) {
2957
- this.declare(`@scr_exc_alert = external global i32, align 64`);
3046
+ const exact = this.mod.workers !== true || this.executableTls;
3047
+ if (this.executableTls) {
3048
+ // Termination stays observable where the polls were dropped for it:
3049
+ // polls, tests inside source loops (exc's loop-poll rule relies on
3050
+ // them every iteration) and tail-position tests (a dead ordered load
3051
+ // keeps recursion from becoming a loop without stack growth) use an
3052
+ // ordered load, which LLVM never deletes, hoists or merges. Every
3053
+ // other test is an unordered load: it reads the same word, but LLVM's
3054
+ // memory analyses no longer treat it as a write, so loads around it
3055
+ // stay forwardable after inlining. Same-thread changes only happen in
3056
+ // runtime calls, which clobber memory.
3057
+ word = this.loadExcFlag(!poll && through === undefined && this.loopDepth === 0);
3058
+ }
3059
+ else if (this.mod.workers === true) {
3060
+ // dso_local: executables link the runtime statically, so the word is
3061
+ // addressed directly instead of through the GOT.
3062
+ this.declare(`@scr_exc_alert = external dso_local global i32, align 64`);
2958
3063
  word = B.tmp();
2959
3064
  B.line(`${word} = load atomic i32, ptr @scr_exc_alert monotonic, align 64`);
2960
3065
  }
@@ -2984,7 +3089,7 @@ export class LlEmitter {
2984
3089
  B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${hit}, i1 false)`);
2985
3090
  const lu = B.newLabel("exc.u");
2986
3091
  const lk = B.newLabel("exc.k");
2987
- if (this.mod.workers === true) {
3092
+ if (!exact) {
2988
3093
  this.declare(`declare zeroext i1 @scr_exc_pending()`);
2989
3094
  const slow = B.newLabel("exc.s");
2990
3095
  B.condBr(cold, slow, lk);
@@ -3016,30 +3121,98 @@ export class LlEmitter {
3016
3121
  * throw Node's RangeError and unwind like any other pending exception. */
3017
3122
  emitStackCheck() {
3018
3123
  const B = this.B;
3019
- const tl = this.mod.workers === true ? "thread_local " : "";
3020
- this.declare(`@scr_stack_guard = external ${tl}global ptr`);
3021
3124
  this.declare(`declare ptr @llvm.stacksave.p0()`);
3022
3125
  this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
3023
3126
  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
3127
  const over = B.newLabel("stack.over");
3033
3128
  const ok = B.newLabel("stack.ok");
3034
- B.condBr(cold, over, ok);
3129
+ const sp = B.tmp();
3130
+ B.line(`${sp} = call ptr @llvm.stacksave.p0()`);
3131
+ const test = (guard) => {
3132
+ const low = B.tmp();
3133
+ const cold = B.tmp();
3134
+ B.line(`${low} = icmp ult ptr ${sp}, ${guard}`);
3135
+ B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${low}, i1 false)`);
3136
+ B.condBr(cold, over, ok);
3137
+ };
3138
+ if (this.stackGuardTsd !== null) {
3139
+ // A Darwin thread-local access is a call to its TLV thunk: read the
3140
+ // guard from its thread-specific-data slot at the runtime-validated
3141
+ // offset instead, and call out only when the runtime found no slot.
3142
+ this.declare(`@scr_stack_guard_tsd = external dso_local global i64`);
3143
+ this.declare(`declare i64 @scr_stack_guard_get()`);
3144
+ const offset = B.tmp();
3145
+ const direct = B.tmp();
3146
+ const likely = B.tmp();
3147
+ B.line(`${offset} = load i64, ptr @scr_stack_guard_tsd`);
3148
+ B.line(`${direct} = icmp ne i64 ${offset}, 0`);
3149
+ B.line(`${likely} = call i1 @llvm.expect.i1(i1 ${direct}, i1 true)`);
3150
+ const fast = B.newLabel("stack.tsd");
3151
+ const slow = B.newLabel("stack.tlv");
3152
+ B.condBr(likely, fast, slow);
3153
+ B.startBlock(fast);
3154
+ const guard = B.tmp();
3155
+ if (this.stackGuardTsd === "aarch64") {
3156
+ this.declare(`declare i64 @llvm.read_register.i64(metadata)`);
3157
+ const thread = B.tmp();
3158
+ const base = B.tmp();
3159
+ const slot = B.tmp();
3160
+ const address = B.tmp();
3161
+ B.line(`${thread} = call i64 @llvm.read_register.i64(metadata !{!"tpidrro_el0"})`);
3162
+ B.line(`${base} = and i64 ${thread}, -8`);
3163
+ B.line(`${slot} = add i64 ${base}, ${offset}`);
3164
+ B.line(`${address} = inttoptr i64 ${slot} to ptr`);
3165
+ B.line(`${guard} = load ptr, ptr ${address}`);
3166
+ }
3167
+ else {
3168
+ const address = B.tmp();
3169
+ B.line(`${address} = inttoptr i64 ${offset} to ptr addrspace(256)`);
3170
+ B.line(`${guard} = load ptr, ptr addrspace(256) ${address}`);
3171
+ }
3172
+ test(guard);
3173
+ B.startBlock(slow);
3174
+ // uintptr_t scr_stack_guard_get(void): an i64 in the C ABI.
3175
+ const raw = B.tmp();
3176
+ const fallback = B.tmp();
3177
+ B.line(`${raw} = call i64 @scr_stack_guard_get()`);
3178
+ B.line(`${fallback} = inttoptr i64 ${raw} to ptr`);
3179
+ test(fallback);
3180
+ }
3181
+ else {
3182
+ const tl = this.mod.workers === true ? "thread_local " : "";
3183
+ this.declare(`@scr_stack_guard = external ${tl}global ptr`);
3184
+ const guard = B.tmp();
3185
+ B.line(`${guard} = load ptr, ptr @scr_stack_guard`);
3186
+ test(guard);
3187
+ }
3035
3188
  B.startBlock(over);
3036
3189
  B.line(`call void @scr_stack_overflow()`);
3037
3190
  this.emitUnwind();
3038
3191
  B.startBlock(ok);
3039
3192
  }
3040
- workerLoopBudget() {
3193
+ /** Whether the emitter follows this call with a pending test (the
3194
+ * conditions in expr-calls.ts). */
3195
+ callTestsPending(e) {
3196
+ switch (e.kind) {
3197
+ case "call":
3198
+ return this.mayThrow.has(e.callee);
3199
+ case "new":
3200
+ return this.mayThrow.has(`%${e.className}.constructor`);
3201
+ case "virtualCall":
3202
+ return this.mayThrowMethods.has(e.method);
3203
+ case "callValue":
3204
+ return this.indirectMayThrow;
3205
+ default:
3206
+ return false;
3207
+ }
3208
+ }
3209
+ workerLoopBudget(loop) {
3041
3210
  if (!this.mod.workers)
3042
3211
  return null;
3212
+ // A loop that runs a pending test on every iteration observes a
3213
+ // terminate request through it; a separate poll would only add work.
3214
+ if (loopTestsEveryIteration(loop, (e) => this.callTestsPending(e)))
3215
+ return null;
3043
3216
  const slot = this.B.slot();
3044
3217
  this.B.entryAllocas.push(`${slot} = alloca i32 ; worker cancellation budget`);
3045
3218
  this.B.line(`store i32 0, ptr ${slot}`);
@@ -3061,7 +3234,7 @@ export class LlEmitter {
3061
3234
  // Calls and throwing operations retain their own checks. Amortize the
3062
3235
  // extra cancellation poll across bounded loop batches so scalar work
3063
3236
  // does not cross the runtime ABI on every iteration.
3064
- this.emitPendingCheck();
3237
+ this.emitPendingCheck(undefined, true);
3065
3238
  B.br(next);
3066
3239
  B.startBlock(next);
3067
3240
  }
@@ -3665,18 +3838,38 @@ export class LlEmitter {
3665
3838
  this.declare(`declare void @scr_box_set_ref(ptr, ptr)`);
3666
3839
  B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${cell})`);
3667
3840
  }
3841
+ /** The payload a TDZ box holds before its declaration runs, when that is
3842
+ * not NULL. A nullable union's value is its arm pointer with NULL as a
3843
+ * unit arm (nullable-unions.ts), so a stored `null`/`undefined` would
3844
+ * read back as the temporal dead zone; such boxes start at the module's
3845
+ * immortal NULLABLE_ABSENT sentinel instead, which no value of the
3846
+ * binding can hold and every RC entry point and the collector skip. */
3847
+ tdzEmptySentinel(t) {
3848
+ if (!this.nullableUnions.of(t))
3849
+ return null;
3850
+ this.needsNullableAbsent = true;
3851
+ this.immortalValues.add(NULLABLE_ABSENT);
3852
+ return NULLABLE_ABSENT;
3853
+ }
3668
3854
  /** The TDZ-guarded read of a boxed binding: an empty payload slot is
3669
3855
  * the temporal dead zone — throw Node's exact catchable ReferenceError
3670
3856
  * (exprs.ts's varRef guard). Scalars then peek the one-element
3671
3857
  * array cell; ref kinds read the box normally (+1). */
3672
- checkTdz(box, name) {
3858
+ checkTdz(box, t, name) {
3673
3859
  const B = this.B;
3674
3860
  const slotp = B.tmp();
3675
3861
  const slotv = B.tmp();
3676
3862
  const empty = B.tmp();
3677
3863
  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`);
3864
+ const sentinel = this.tdzEmptySentinel(t);
3865
+ if (sentinel !== null) {
3866
+ B.line(`${slotv} = load ptr, ptr ${slotp}`);
3867
+ B.line(`${empty} = icmp eq ptr ${slotv}, ${sentinel}`);
3868
+ }
3869
+ else {
3870
+ B.line(`${slotv} = load i64, ptr ${slotp}`);
3871
+ B.line(`${empty} = icmp eq i64 ${slotv}, 0`);
3872
+ }
3680
3873
  this.throwIfUninitialized(empty, name);
3681
3874
  return slotv;
3682
3875
  }
@@ -3713,7 +3906,7 @@ export class LlEmitter {
3713
3906
  B.startBlock(lk);
3714
3907
  }
3715
3908
  tdzBoxRead(box, t, name) {
3716
- const slotv = this.checkTdz(box, name);
3909
+ const slotv = this.checkTdz(box, t, name);
3717
3910
  const B = this.B;
3718
3911
  const acc = boxAccess(t);
3719
3912
  if (acc === "ref")
@@ -3733,7 +3926,7 @@ export class LlEmitter {
3733
3926
  writeBindingBox(box, local, value, initializes = false, borrowed = false) {
3734
3927
  if (local.tdz) {
3735
3928
  const first = initializes || !local.mutable;
3736
- const slotv = first ? null : this.checkTdz(box, local.name);
3929
+ const slotv = first ? null : this.checkTdz(box, local.type, local.name);
3737
3930
  const acc = boxAccess(local.type);
3738
3931
  if (acc !== "ref") {
3739
3932
  if (first)
@@ -3912,6 +4105,8 @@ export class LlEmitter {
3912
4105
  this.captureIds = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId));
3913
4106
  this.integerLoopBindings.clear();
3914
4107
  this.countedLoopsEnabled = this.debug === null && !fn.async && !fn.generator;
4108
+ this.finalFieldBorrows =
4109
+ !fn.async && !fn.generator && constructorClass(fn.name) === null && this.finalFields.size > 0;
3915
4110
  this.byteWindowsEnabled = this.countedLoopsEnabled;
3916
4111
  this.byteWindowEntries =
3917
4112
  this.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
@@ -3924,8 +4119,8 @@ export class LlEmitter {
3924
4119
  this.debug === null
3925
4120
  ? findLoopArrayBorrows(fn, this.callLifetimes, this.referenceEffects, this.optionalArrayReads)
3926
4121
  : 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);
4122
+ this.localArrayReads = findLocalArrayReads(fn, this.fnByName, this.unionsById, this.referenceEffects.functions, this.callLifetimes, this.optionalArrayReads, this.loopArrayBorrows, this.debug === null ? this.referenceEffects : undefined);
4123
+ this.callArrayReads = findCallArrayReads(fn, this.optionalArrayReads, this.referenceEffects.functions, this.callLifetimes, this.debug === null ? this.referenceEffects : undefined);
3929
4124
  this.scalarStringSlices =
3930
4125
  this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
3931
4126
  this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
@@ -3938,6 +4133,7 @@ export class LlEmitter {
3938
4133
  this.chainSlots.clear();
3939
4134
  this.finallyStack = [];
3940
4135
  this.tryStack = [];
4136
+ this.loopDepth = 0;
3941
4137
  this.currentReturnType = fn.returnType;
3942
4138
  this.currentGenerator = fn.generator ?? null;
3943
4139
  this.currentBorrowedReturn = this.borrowedReturns.has(fn.name);
@@ -4100,7 +4296,7 @@ export class LlEmitter {
4100
4296
  if (this.stackChecks?.has(fn.name))
4101
4297
  this.emitStackCheck();
4102
4298
  if (this.workerEntryPolls?.has(fn.name))
4103
- this.emitPendingCheck();
4299
+ this.emitPendingCheck(undefined, true);
4104
4300
  this.emitStmts(fn.body);
4105
4301
  // Implicit exit of a void function: release the function scope unless
4106
4302
  // the body already terminated its final block (return, or a throw
@@ -4250,11 +4446,24 @@ export class LlEmitter {
4250
4446
  return this.canBorrowCallArgument(value.value);
4251
4447
  if (value.kind === "libCall")
4252
4448
  return value.fn === "error.nodeThrow";
4449
+ // A final field never changes outside constructors: while its object is
4450
+ // borrowable, so is the field's value. Nullable-pointer fields have no
4451
+ // box to lend and keep their owning path.
4452
+ if (value.kind === "fieldGet" &&
4453
+ this.finalFieldBorrows &&
4454
+ isRefCounted(value.type) &&
4455
+ this.finalFields.isFinal(value.className, value.field) &&
4456
+ !this.nullableFieldGet(value))
4457
+ return this.canBorrowReceiver(value.obj) && this.canBorrowCallArgument(value.obj);
4253
4458
  if (value.kind === "ternary")
4254
4459
  return (this.canBorrowReceiver(value.then) &&
4255
4460
  this.canBorrowReceiver(value.else_) &&
4256
4461
  this.canBorrowCallArgument(value.then) &&
4257
4462
  this.canBorrowCallArgument(value.else_));
4463
+ // An optional chain's bound receiver stays unchanged until its body
4464
+ // finishes (see emitReadReceiver).
4465
+ if (value.kind === "chainRecv")
4466
+ return this.chainSlots.has(value.id);
4258
4467
  if (value.kind !== "varRef")
4259
4468
  return false;
4260
4469
  const binding = this.binding(value.localId);
@@ -4296,11 +4505,16 @@ export class LlEmitter {
4296
4505
  emitStmt(s) {
4297
4506
  const previous = this.B.debugLocation;
4298
4507
  this.B.debugLocation = this.debug?.location(s.loc, this.debugScope) ?? null;
4508
+ const loop = s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf";
4509
+ if (loop)
4510
+ this.loopDepth++;
4299
4511
  try {
4300
4512
  this.emitStmtBody(s);
4301
4513
  }
4302
4514
  finally {
4303
4515
  this.B.debugLocation = previous;
4516
+ if (loop)
4517
+ this.loopDepth--;
4304
4518
  }
4305
4519
  }
4306
4520
  emitStmtBody(s) {
@@ -4397,8 +4611,14 @@ export class LlEmitter {
4397
4611
  B.line(`${box} = ${boxNew} ; let ${b.local.name} (boxed)`);
4398
4612
  B.line(`store ptr ${box}, ptr ${b.slot}`);
4399
4613
  this.scopes[this.scopes.length - 1].push({ slot: b.slot, type: b.type, boxed: true });
4400
- if (s.init === null)
4614
+ if (s.init === null) {
4615
+ // An empty TDZ box whose payload can legitimately be NULL
4616
+ // starts at its distinct sentinel (tdzEmptySentinel).
4617
+ const sentinel = b.local.tdz === true ? this.tdzEmptySentinel(b.type) : null;
4618
+ if (sentinel !== null)
4619
+ B.line(`store ptr ${sentinel}, ptr ${this.boxSlot(box)}`);
4401
4620
  break;
4621
+ }
4402
4622
  const v = this.emitExpr(s.init);
4403
4623
  if (isRefCounted(v.type))
4404
4624
  this.moveTemp(v); // the box takes ownership
@@ -4545,7 +4765,7 @@ export class LlEmitter {
4545
4765
  B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
4546
4766
  this.markMemoryPointer(lenPtr, "array:header");
4547
4767
  B.line(`${len} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`);
4548
- B.line(`${lenNumber} = uitofp ${this.sizeType} ${len} to double`);
4768
+ B.line(`${lenNumber} = sitofp ${this.sizeType} ${len} to double`);
4549
4769
  B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`);
4550
4770
  B.condBr(same, done, resize);
4551
4771
  B.startBlock(resize);
@@ -4833,7 +5053,7 @@ export class LlEmitter {
4833
5053
  break;
4834
5054
  }
4835
5055
  case "while": {
4836
- const workerBudget = this.workerLoopBudget();
5056
+ const workerBudget = this.workerLoopBudget(s);
4837
5057
  const lc = B.newLabel("loop.c");
4838
5058
  const lb = B.newLabel("loop.b");
4839
5059
  const le = B.newLabel("loop.e");
@@ -4858,7 +5078,7 @@ export class LlEmitter {
4858
5078
  break;
4859
5079
  }
4860
5080
  case "doWhile": {
4861
- const workerBudget = this.workerLoopBudget();
5081
+ const workerBudget = this.workerLoopBudget(s);
4862
5082
  // Body first (runs at least once); continue jumps to the CONDITION.
4863
5083
  const lb = B.newLabel("loop.b");
4864
5084
  const lc = B.newLabel("loop.c");
@@ -4923,12 +5143,39 @@ export class LlEmitter {
4923
5143
  }
4924
5144
  let countedJoin = null;
4925
5145
  if (countedLoop?.guarded) {
4926
- const bound = this.emitExpr(countedLoop.limit);
4927
- const safe = B.tmp();
5146
+ let safe = "true";
5147
+ if (countedLoop.limitGuarded) {
5148
+ const bound = this.emitExpr(countedLoop.limit);
5149
+ safe = B.tmp();
5150
+ B.line(`${safe} = fcmp ${countedLoop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(countedLoop.guardLimit)}`);
5151
+ }
5152
+ if (countedLoop.startGuarded) {
5153
+ // An exact integer of at most 2^53 - 1 in magnitude, and not -0
5154
+ // (the bit patterns of the value and its round trip agree).
5155
+ // NaN and infinities fail the ordered range tests.
5156
+ const start = this.emitExpr(countedLoop.start);
5157
+ const low = B.tmp(), high = B.tmp(), raw = B.tmp(), wide = B.tmp(), back = B.tmp(), bits = B.tmp(), backBits = B.tmp(), exact = B.tmp(), inRange = B.tmp(), ok = B.tmp();
5158
+ B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`);
5159
+ B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
5160
+ B.line(`${inRange} = and i1 ${low}, ${high}`);
5161
+ B.line(`${raw} = fptosi double ${start.name} to i64`);
5162
+ B.line(`${wide} = freeze i64 ${raw}`);
5163
+ B.line(`${back} = sitofp i64 ${wide} to double`);
5164
+ B.line(`${bits} = bitcast double ${start.name} to i64`);
5165
+ B.line(`${backBits} = bitcast double ${back} to i64`);
5166
+ B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
5167
+ B.line(`${ok} = and i1 ${inRange}, ${exact}`);
5168
+ if (safe === "true")
5169
+ safe = ok;
5170
+ else {
5171
+ const both = B.tmp();
5172
+ B.line(`${both} = and i1 ${safe}, ${ok}`);
5173
+ safe = both;
5174
+ }
5175
+ }
4928
5176
  const fast = B.newLabel("counted.fast");
4929
5177
  const slow = B.newLabel("counted.slow");
4930
5178
  countedJoin = B.newLabel("counted.done");
4931
- B.line(`${safe} = fcmp ${countedLoop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(countedLoop.guardLimit)}`);
4932
5179
  B.condBr(safe, fast, slow);
4933
5180
  B.startBlock(slow);
4934
5181
  this.countedLoopsEnabled = false;
@@ -5009,7 +5256,7 @@ export class LlEmitter {
5009
5256
  };
5010
5257
  const lu = s.update || capturedLets.length > 0 ? B.newLabel("loop.u") : lc;
5011
5258
  freshenBindings();
5012
- const workerBudget = this.workerLoopBudget();
5259
+ const workerBudget = this.workerLoopBudget(s);
5013
5260
  B.br(lc);
5014
5261
  B.startBlock(lc);
5015
5262
  this.emitWorkerLoopCheck(workerBudget);
@@ -5117,7 +5364,7 @@ export class LlEmitter {
5117
5364
  const lb = B.newLabel("fof.b");
5118
5365
  const lu = B.newLabel("fof.u");
5119
5366
  const le = B.newLabel("fof.e");
5120
- const workerBudget = this.workerLoopBudget();
5367
+ const workerBudget = this.workerLoopBudget(s);
5121
5368
  B.br(lc);
5122
5369
  B.startBlock(lc);
5123
5370
  this.emitWorkerLoopCheck(workerBudget);
@@ -5136,7 +5383,7 @@ export class LlEmitter {
5136
5383
  B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
5137
5384
  this.markMemoryPointer(lenPtr, "array:header");
5138
5385
  B.line(`${rawLen} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`);
5139
- B.line(`${len} = uitofp ${this.sizeType} ${rawLen} to double`);
5386
+ B.line(`${len} = sitofp ${this.sizeType} ${rawLen} to double`);
5140
5387
  B.line(`${inBounds} = fcmp olt double ${i}, ${len}`);
5141
5388
  cur = i;
5142
5389
  }
@@ -5994,6 +6241,16 @@ export class LlEmitter {
5994
6241
  this.B.line(`${value} = load ptr, ptr ${binding.slot}`);
5995
6242
  return { name: value, type: e.type };
5996
6243
  }
6244
+ if (e.kind === "chainRecv") {
6245
+ // The chain's bind slot holds its receiver unchanged until the body
6246
+ // finishes, owned by the slot itself or by the chain's receiver.
6247
+ const bound = this.chainSlots.get(e.id);
6248
+ if (!bound)
6249
+ throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`);
6250
+ const value = this.B.tmp();
6251
+ this.B.line(`${value} = load ${this.llType(bound.type)}, ptr ${bound.name}`);
6252
+ return { name: value, type: e.type };
6253
+ }
5997
6254
  if (e.kind === "arrayGet" && this.canBorrowReceiver(e)) {
5998
6255
  // The array keeps the element alive until the consumer; a missing
5999
6256
  // element still traps exactly like the owned read.
@@ -6229,6 +6486,8 @@ export class LlEmitter {
6229
6486
  }
6230
6487
  canBorrowReceiver(e) {
6231
6488
  switch (e.kind) {
6489
+ case "chainRecv":
6490
+ return this.chainSlots.has(e.id);
6232
6491
  case "varRef": {
6233
6492
  const binding = this.binding(e.localId);
6234
6493
  return (binding.kind !== "boxed" ||