@scriptc/compiler 0.2.9 → 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 (147) hide show
  1. package/dist/backend/cycle-analysis.d.ts +56 -0
  2. package/dist/backend/cycle-analysis.js +175 -14
  3. package/dist/backend/cycle-analysis.js.map +1 -1
  4. package/dist/backend/cycle-analysis.test.js +94 -1
  5. package/dist/backend/cycle-analysis.test.js.map +1 -1
  6. package/dist/backend/cycle-mutability.d.ts +2 -6
  7. package/dist/backend/cycle-mutability.js +3 -12
  8. package/dist/backend/cycle-mutability.js.map +1 -1
  9. package/dist/backend/immutability.d.ts +57 -0
  10. package/dist/backend/immutability.js +178 -0
  11. package/dist/backend/immutability.js.map +1 -0
  12. package/dist/backend/immutability.test.d.ts +1 -0
  13. package/dist/backend/immutability.test.js +131 -0
  14. package/dist/backend/immutability.test.js.map +1 -0
  15. package/dist/backend/llvm/borrowed-inputs.d.ts +9 -2
  16. package/dist/backend/llvm/borrowed-inputs.js +31 -3
  17. package/dist/backend/llvm/borrowed-inputs.js.map +1 -1
  18. package/dist/backend/llvm/classes.d.ts +28 -0
  19. package/dist/backend/llvm/classes.js +45 -5
  20. package/dist/backend/llvm/classes.js.map +1 -1
  21. package/dist/backend/llvm/classes.test.js +129 -1
  22. package/dist/backend/llvm/classes.test.js.map +1 -1
  23. package/dist/backend/llvm/common.d.ts +13 -0
  24. package/dist/backend/llvm/common.js +13 -0
  25. package/dist/backend/llvm/common.js.map +1 -1
  26. package/dist/backend/llvm/dense-array-access.js +5 -4
  27. package/dist/backend/llvm/dense-array-access.js.map +1 -1
  28. package/dist/backend/llvm/dense-array-access.test.js +9 -1
  29. package/dist/backend/llvm/dense-array-access.test.js.map +1 -1
  30. package/dist/backend/llvm/dyn.d.ts +2 -0
  31. package/dist/backend/llvm/dyn.js +3 -3
  32. package/dist/backend/llvm/dyn.js.map +1 -1
  33. package/dist/backend/llvm/emitter.d.ts +64 -2
  34. package/dist/backend/llvm/emitter.js +390 -45
  35. package/dist/backend/llvm/emitter.js.map +1 -1
  36. package/dist/backend/llvm/emitter.test.js +18 -0
  37. package/dist/backend/llvm/emitter.test.js.map +1 -1
  38. package/dist/backend/llvm/expr-calls.js +14 -6
  39. package/dist/backend/llvm/expr-calls.js.map +1 -1
  40. package/dist/backend/llvm/expr-containers.js +30 -7
  41. package/dist/backend/llvm/expr-containers.js.map +1 -1
  42. package/dist/backend/llvm/expr-primitives.js +30 -3
  43. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  44. package/dist/backend/llvm/expr-primitives.test.js +53 -0
  45. package/dist/backend/llvm/expr-primitives.test.js.map +1 -1
  46. package/dist/backend/llvm/expr-stream-bridges.js +4 -4
  47. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  48. package/dist/backend/llvm/local-array-reads.js +3 -3
  49. package/dist/backend/llvm/local-array-reads.js.map +1 -1
  50. package/dist/backend/llvm/loop-array-borrows.d.ts +5 -2
  51. package/dist/backend/llvm/loop-array-borrows.js +27 -8
  52. package/dist/backend/llvm/loop-array-borrows.js.map +1 -1
  53. package/dist/backend/llvm/map-number-lookup.d.ts +7 -0
  54. package/dist/backend/llvm/map-number-lookup.js +18 -1
  55. package/dist/backend/llvm/map-number-lookup.js.map +1 -1
  56. package/dist/backend/llvm/map-read-lifetimes.d.ts +5 -0
  57. package/dist/backend/llvm/map-read-lifetimes.js +14 -4
  58. package/dist/backend/llvm/map-read-lifetimes.js.map +1 -1
  59. package/dist/backend/llvm/map-read-lifetimes.test.js +5 -0
  60. package/dist/backend/llvm/map-read-lifetimes.test.js.map +1 -1
  61. package/dist/backend/llvm/reference-writes.d.ts +124 -0
  62. package/dist/backend/llvm/reference-writes.js +1090 -0
  63. package/dist/backend/llvm/reference-writes.js.map +1 -0
  64. package/dist/backend/llvm/shapes.d.ts +8 -5
  65. package/dist/backend/llvm/shapes.js +36 -11
  66. package/dist/backend/llvm/shapes.js.map +1 -1
  67. package/dist/backend/llvm/string-lifetimes.test.js +39 -10
  68. package/dist/backend/llvm/string-lifetimes.test.js.map +1 -1
  69. package/dist/backend/llvm/string-reads.d.ts +3 -0
  70. package/dist/backend/llvm/string-reads.js +31 -0
  71. package/dist/backend/llvm/string-reads.js.map +1 -1
  72. package/dist/backend/llvm/switch-dispatch.d.ts +2 -2
  73. package/dist/backend/llvm/switch-dispatch.js +35 -22
  74. package/dist/backend/llvm/switch-dispatch.js.map +1 -1
  75. package/dist/backend/llvm/switch-dispatch.test.js +28 -10
  76. package/dist/backend/llvm/switch-dispatch.test.js.map +1 -1
  77. package/dist/backend/llvm/walk-borrows.d.ts +8 -1
  78. package/dist/backend/llvm/walk-borrows.js +15 -4
  79. package/dist/backend/llvm/walk-borrows.js.map +1 -1
  80. package/dist/backend/may-throw.d.ts +36 -2
  81. package/dist/backend/may-throw.js +308 -12
  82. package/dist/backend/may-throw.js.map +1 -1
  83. package/dist/backend/may-throw.test.js +146 -1
  84. package/dist/backend/may-throw.test.js.map +1 -1
  85. package/dist/backend/publication.d.ts +5 -0
  86. package/dist/backend/publication.js +10 -0
  87. package/dist/backend/publication.js.map +1 -1
  88. package/dist/backend/runtime-abi.d.ts +2 -2
  89. package/dist/backend/runtime-abi.js +2 -2
  90. package/dist/backend/runtime-pack-targets.test.js +1 -1
  91. package/dist/backend/runtime-pack.test.js +1 -1
  92. package/dist/contracts/check.d.ts +8 -0
  93. package/dist/contracts/check.js +1577 -0
  94. package/dist/contracts/check.js.map +1 -0
  95. package/dist/executable/prepare.js +9 -0
  96. package/dist/executable/prepare.js.map +1 -1
  97. package/dist/frontend/contract-types.d.ts +50 -0
  98. package/dist/frontend/contract-types.js +232 -0
  99. package/dist/frontend/contract-types.js.map +1 -0
  100. package/dist/frontend/lowering/coercions/unions.d.ts +12 -0
  101. package/dist/frontend/lowering/coercions/unions.js +63 -0
  102. package/dist/frontend/lowering/coercions/unions.js.map +1 -1
  103. package/dist/frontend/lowering/expressions/optional-chains.js +4 -1
  104. package/dist/frontend/lowering/expressions/optional-chains.js.map +1 -1
  105. package/dist/frontend/lowering/lower-calls.js +9 -2
  106. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  107. package/dist/frontend/lowering/lower-exprs.js +83 -1
  108. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  109. package/dist/frontend/lowering/lowerer.d.ts +9 -0
  110. package/dist/frontend/lowering/lowerer.js +47 -0
  111. package/dist/frontend/lowering/lowerer.js.map +1 -1
  112. package/dist/frontend/lowering/narrowing-stability.d.ts +100 -0
  113. package/dist/frontend/lowering/narrowing-stability.js +1174 -0
  114. package/dist/frontend/lowering/narrowing-stability.js.map +1 -0
  115. package/dist/frontend/lowering/narrowing-stability.test.d.ts +1 -0
  116. package/dist/frontend/lowering/narrowing-stability.test.js +150 -0
  117. package/dist/frontend/lowering/narrowing-stability.test.js.map +1 -0
  118. package/dist/frontend/program.js +1 -1
  119. package/dist/frontend/type-mapper.js +43 -0
  120. package/dist/frontend/type-mapper.js.map +1 -1
  121. package/dist/ir/int32-slots.d.ts +26 -0
  122. package/dist/ir/int32-slots.js +147 -47
  123. package/dist/ir/int32-slots.js.map +1 -1
  124. package/dist/ir/integer-loops.js +3 -0
  125. package/dist/ir/integer-loops.js.map +1 -1
  126. package/dist/ir/integer-loops.test.js +26 -1
  127. package/dist/ir/integer-loops.test.js.map +1 -1
  128. package/dist/ir/integer-ranges.d.ts +20 -0
  129. package/dist/ir/integer-ranges.js +178 -21
  130. package/dist/ir/integer-ranges.js.map +1 -1
  131. package/dist/ir/integer-ranges.test.js +148 -3
  132. package/dist/ir/integer-ranges.test.js.map +1 -1
  133. package/dist/ir/ir.d.ts +32 -0
  134. package/dist/ir/ir.js.map +1 -1
  135. package/dist/ir/serialize.d.ts +5 -1
  136. package/dist/ir/serialize.js +8 -0
  137. package/dist/ir/serialize.js.map +1 -1
  138. package/dist/ir/unshare.d.ts +14 -0
  139. package/dist/ir/unshare.js +60 -0
  140. package/dist/ir/unshare.js.map +1 -0
  141. package/dist/ir/unshare.test.d.ts +1 -0
  142. package/dist/ir/unshare.test.js +115 -0
  143. package/dist/ir/unshare.test.js.map +1 -0
  144. package/dist/library/prepare.js +9 -0
  145. package/dist/library/prepare.js.map +1 -1
  146. package/package.json +20 -20
  147. package/surface-manifest.json +1 -1
@@ -36,6 +36,7 @@ import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
36
36
  import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
37
37
  import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
38
38
  import { analyzeIntegerRanges, INT32_RANGE } from "../../ir/integer-ranges.js";
39
+ import { unshareNodes } from "../../ir/unshare.js";
39
40
  import { analyzeInt32Slots } from "../../ir/int32-slots.js";
40
41
  import { analyzeFinalFields, constructorClass } from "../../ir/final-fields.js";
41
42
  import { foldConstantGlobals } from "../../ir/constant-globals.js";
@@ -56,10 +57,11 @@ import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js"
56
57
  import { emitDenseArrayGet, emitDenseArraySet } from "./dense-array-access.js";
57
58
  import { emitStackMapRead, findMapReadLifetimes, matchMapRead, } from "./map-read-lifetimes.js";
58
59
  import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
59
- import { emitBorrowedInput } from "./borrowed-inputs.js";
60
+ import { borrowableInputs, emitBorrowedInput } from "./borrowed-inputs.js";
60
61
  import { analyzeWalks, findWalkBorrows } from "./walk-borrows.js";
61
62
  import { findReboundParameters } from "./rebound-parameters.js";
62
63
  import { ReferenceEffects } from "./reference-effects.js";
64
+ import { ReferenceWrites } from "./reference-writes.js";
63
65
  import { LlvmDebugInfo } from "./debug-info.js";
64
66
  import { StackCallbacks } from "./stack-callbacks.js";
65
67
  import { findLoopArrayBorrows } from "./loop-array-borrows.js";
@@ -75,6 +77,7 @@ import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
75
77
  import { emitDynamicExpr } from "./expr-dynamic.js";
76
78
  import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js";
77
79
  import { PublishWalkers } from "./publish.js";
80
+ import { computeImmutableFields, stableGlobals } from "../immutability.js";
78
81
  import { computePublishedTypes } from "../publication.js";
79
82
  import { emitJsInteropExpr, emitExpr } from "./expr-dispatch.js";
80
83
  import { emitJsMarshal, emitJsOp, emitJsExit, islandAdapter, islandTypedAdapter, } from "./expr-island.js";
@@ -97,7 +100,7 @@ import { LlWalkers } from "./walkers.js";
97
100
  import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js";
98
101
  import { emitNullablePresent, emitUnionPeek, emitUnionTag } from "./union-repr.js";
99
102
  import { literalCapWord, literalCapWord32 } from "./string-key-hash.js";
100
- 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";
101
104
  import { STRING_READ_HELPERS } from "./string-reads.js";
102
105
  import { emitConcatInputs, emitMixedConcat, numberPart, stringParts, } from "./string-construction.js";
103
106
  import { arrNewCall, boxAccess, boxNewCall, boxReleaseSym, computeTraced, elemAccess, emitInlineRcHelpers, emitNullableRetainWrappers, FN_ATTRS, inlineRcDecls, llFieldType, releaseSym, retainSym, traceArg, typedRefConstructor, vAdapters, } from "./shapes.js";
@@ -127,14 +130,18 @@ function ffiCallbackDummyLl(callback) {
127
130
  return "i32 0";
128
131
  }
129
132
  }
130
- 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);
131
137
  // Keep source storage intact for debugger inspection in dev builds.
132
138
  return new LlEmitter(options.debugSources === undefined
133
139
  ? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
134
140
  : mod, options).emit();
135
141
  }
136
142
  /** Keep large translation units below the host's single-string limit. */
137
- export function emitLlvmModuleSource(mod, options = {}) {
143
+ export function emitLlvmModuleSource(input, options = {}) {
144
+ const mod = unshareNodes(input);
138
145
  const parts = new LlEmitter(options.debugSources === undefined
139
146
  ? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
140
147
  : mod, options).emitParts();
@@ -302,6 +309,12 @@ export class LlEmitter {
302
309
  * fiberless-async analysis scans it for suspension entry points. */
303
310
  fnDefText = new Map();
304
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;
305
318
  optionalArrayReads;
306
319
  checkedNarrows;
307
320
  callArrayReads = new Map();
@@ -345,6 +358,9 @@ export class LlEmitter {
345
358
  ffiHasRetainedCallback;
346
359
  ffiHasForeignCallback;
347
360
  globalTypes = new Map();
361
+ /** Module constants that keep one value once initialized
362
+ * (immutability.ts stableGlobals). */
363
+ stableGlobals = new Set();
348
364
  tdzGlobals = new Map();
349
365
  constantNumericTables;
350
366
  /** May-throw analysis (computeMayThrow): pending
@@ -360,6 +376,13 @@ export class LlEmitter {
360
376
  /** Static types whose values can be published (@scriptc/threads): their
361
377
  * stores carry the frozen-object guard. Null without publish calls. */
362
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;
363
386
  /** Method names with at least one may-throw implementation — the
364
387
  * virtualCall pending check's key. */
365
388
  mayThrowMethods = new Set();
@@ -436,6 +459,12 @@ export class LlEmitter {
436
459
  initializerBindings;
437
460
  /** Whole-program int32 storage and parameter/result facts. */
438
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;
439
468
  numericLocals = new Map();
440
469
  captureIds = new Set();
441
470
  /** Active induction bindings retain their width independently of size_t. */
@@ -495,6 +524,8 @@ export class LlEmitter {
495
524
  this.constantNumericTables = findConstantNumericTables(mod);
496
525
  this.initializerBindings = findInitializerBindings(mod);
497
526
  this.int32Slots = analyzeInt32Slots(mod);
527
+ this.immortalGlobals = new Set(mod.contracts?.immortalGlobals ?? []);
528
+ this.programLifetime = new Set(mod.contracts?.programLifetime ?? []);
498
529
  this.finalFields = analyzeFinalFields(mod);
499
530
  this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
500
531
  this.ffiExtendNarrowIntegers =
@@ -549,6 +580,8 @@ export class LlEmitter {
549
580
  this.workerEntryPolls = mt.workerEntryPolls ?? null;
550
581
  this.stackChecks = mt.stackChecks ?? null;
551
582
  this.publishedTypes = computePublishedTypes(mod);
583
+ this.immutableFields = computeImmutableFields(mod);
584
+ this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
552
585
  this.indirectMayThrow = mt.indirect || mod.workers === true;
553
586
  for (const cls of mod.classes ?? []) {
554
587
  for (const m of cls.methods ?? []) {
@@ -568,6 +601,8 @@ export class LlEmitter {
568
601
  this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById, (id) => this.nullableUnions.has(id));
569
602
  this.checkedNarrows = new CheckedNarrows(this.fnByName);
570
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);
571
606
  const walkParameters = new Map();
572
607
  if (this.debug === null) {
573
608
  const host = {
@@ -575,9 +610,10 @@ export class LlEmitter {
575
610
  borrowsWithoutOwning: (e) => this.borrowsWithoutOwning(e),
576
611
  };
577
612
  for (const fn of this.fnByName.values()) {
578
- if (!this.referenceEffects.functions.has(fn.name))
613
+ const preserving = this.referenceEffects.functions.has(fn.name);
614
+ if (!preserving && this.referenceWrites === null)
579
615
  continue;
580
- const walks = findWalkBorrows(fn, host);
616
+ const walks = findWalkBorrows(fn, preserving ? host : this.unwrittenWalkHost(fn, host));
581
617
  if (walks.size === 0)
582
618
  continue;
583
619
  this.walkBorrowsByFunction.set(fn.name, walks);
@@ -661,6 +697,7 @@ export class LlEmitter {
661
697
  },
662
698
  pendingTestLines: (dest) => this.pendingTestLines(dest),
663
699
  virtualEntry: (implFn) => this.virtualEntry(implFn),
700
+ programLifetime: this.programLifetime,
664
701
  };
665
702
  this.walkers = new LlWalkers(this.shapeHost);
666
703
  this.dyn = new LlDyn(this.shapeHost);
@@ -682,6 +719,7 @@ export class LlEmitter {
682
719
  if (g.tdz)
683
720
  this.tdzGlobals.set(g.id, g.name);
684
721
  }
722
+ this.stableGlobals = stableGlobals(mod);
685
723
  // Runtime error, EventEmitter, and stream classes have known layouts.
686
724
  // Subclasses embed the ScrEmitter/ScrStream prefixes from classes.ts.
687
725
  const classes = mod.classes ?? [];
@@ -1403,12 +1441,14 @@ export class LlEmitter {
1403
1441
  `%ScrFfiTable = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, i8, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`,
1404
1442
  `%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr, double, ptr, ptr }`,
1405
1443
  // 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 }`,
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 }`,
1412
1452
  // The ScrMap prefix { rc, key_kind, val_kind, val_retain, val_release,
1413
1453
  // val_trace, key_retain, key_release, key_trace } — the inline RC
1414
1454
  // fast paths test the two trace slots (shapes.ts).
@@ -1421,9 +1461,13 @@ export class LlEmitter {
1421
1461
  ...(this.decls.has(NUMBER_MAP_ENTRY_DECL)
1422
1462
  ? [
1423
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} }`,
1424
- `%ScrMapEntry = type { i64, i64, i64 }`,
1425
1464
  ]
1426
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
+ : []),
1427
1471
  // The runtime error prefix { rc, vt, name, message, code, cause } and the
1428
1472
  // class-object shape { rc, pre, post, ctor, name } — field reads on
1429
1473
  // builtin errors and classval loads GEP through these.
@@ -1551,7 +1595,7 @@ export class LlEmitter {
1551
1595
  // strings — a no-op.
1552
1596
  const n = inst.slots.length;
1553
1597
  const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`;
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 }`);
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 }`);
1555
1599
  }
1556
1600
  if (this.templateStringsInstances.size > 0)
1557
1601
  out.push(``);
@@ -4086,6 +4130,7 @@ export class LlEmitter {
4086
4130
  const numericFn = withInitializerBindings(fn, initializerBindings);
4087
4131
  this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
4088
4132
  this.borrowedParameters.clear();
4133
+ this.referenceWrites?.enter(fn);
4089
4134
  this.reboundOwnerSlots.clear();
4090
4135
  this.projectedParameters.clear();
4091
4136
  const projectedParameterIndexes = this.callLifetimes.parameters.get(fn.name);
@@ -4117,7 +4162,7 @@ export class LlEmitter {
4117
4162
  this.integerArrayBindings.clear();
4118
4163
  this.loopArrayBorrows =
4119
4164
  this.debug === null
4120
- ? findLoopArrayBorrows(fn, this.callLifetimes, this.referenceEffects, this.optionalArrayReads)
4165
+ ? findLoopArrayBorrows(fn, this.callLifetimes, this.referenceEffects, this.optionalArrayReads, this.referenceWrites)
4121
4166
  : new Set();
4122
4167
  this.localArrayReads = findLocalArrayReads(fn, this.fnByName, this.unionsById, this.referenceEffects.functions, this.callLifetimes, this.optionalArrayReads, this.loopArrayBorrows, this.debug === null ? this.referenceEffects : undefined);
4123
4168
  this.callArrayReads = findCallArrayReads(fn, this.optionalArrayReads, this.referenceEffects.functions, this.callLifetimes, this.debug === null ? this.referenceEffects : undefined);
@@ -4137,6 +4182,7 @@ export class LlEmitter {
4137
4182
  this.currentReturnType = fn.returnType;
4138
4183
  this.currentGenerator = fn.generator ?? null;
4139
4184
  this.currentBorrowedReturn = this.borrowedReturns.has(fn.name);
4185
+ this.inConstructor = this.constructorFunctions.has(fn.name);
4140
4186
  this.currentWasiCoro = null;
4141
4187
  this.logArgSlots = 0;
4142
4188
  if (this.wasi && (fn.async === true || fn.generator !== undefined)) {
@@ -4297,7 +4343,7 @@ export class LlEmitter {
4297
4343
  this.emitStackCheck();
4298
4344
  if (this.workerEntryPolls?.has(fn.name))
4299
4345
  this.emitPendingCheck(undefined, true);
4300
- this.emitStmts(fn.body);
4346
+ this.emitStmts(fn.body, true);
4301
4347
  // Implicit exit of a void function: release the function scope unless
4302
4348
  // the body already terminated its final block (return, or a throw
4303
4349
  // whose unwind released everything down to depth 0).
@@ -4363,7 +4409,14 @@ export class LlEmitter {
4363
4409
  if (fn.captures !== undefined)
4364
4410
  params.unshift("ptr %sc_env");
4365
4411
  const ret = this.llType(fn.returnType);
4366
- 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}`;
4367
4420
  const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`;
4368
4421
  const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
4369
4422
  const body = `define internal ${ret} @${symbol}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
@@ -4460,6 +4513,10 @@ export class LlEmitter {
4460
4513
  this.canBorrowReceiver(value.else_) &&
4461
4514
  this.canBorrowCallArgument(value.then) &&
4462
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);
4463
4520
  // An optional chain's bound receiver stays unchanged until its body
4464
4521
  // finishes (see emitReadReceiver).
4465
4522
  if (value.kind === "chainRecv")
@@ -4467,6 +4524,9 @@ export class LlEmitter {
4467
4524
  if (value.kind !== "varRef")
4468
4525
  return false;
4469
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);
4470
4530
  return (binding.kind === "local" &&
4471
4531
  binding.local !== undefined &&
4472
4532
  (!binding.local.mutable ||
@@ -4476,15 +4536,212 @@ export class LlEmitter {
4476
4536
  !binding.local.boxed &&
4477
4537
  !binding.local.tdz);
4478
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
+ }
4479
4726
  // ── statements ──────────────────────────────────────────────────────────
4480
- emitStmts(stmts) {
4481
- for (const s of stmts) {
4482
- // Statements after a terminator are unreachable (dead code after
4483
- // return/break/continue). Skip them so no dropped SSA definition
4484
- // can leak forward.
4485
- if (this.B.isTerminated())
4486
- return;
4487
- 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;
4488
4745
  }
4489
4746
  }
4490
4747
  /** Emits a block in its own lexical scope (refcounted locals released at
@@ -4496,7 +4753,7 @@ export class LlEmitter {
4496
4753
  const scope = [];
4497
4754
  this.scopes.push(scope);
4498
4755
  setup?.(scope);
4499
- this.emitStmts(stmts);
4756
+ this.emitStmts(stmts, true);
4500
4757
  const ended = endsWithJump(stmts);
4501
4758
  this.scopes.pop();
4502
4759
  if (!ended)
@@ -4558,7 +4815,9 @@ export class LlEmitter {
4558
4815
  }
4559
4816
  const localRead = this.localArrayReads.get(s.localId);
4560
4817
  if (localRead) {
4561
- 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);
4562
4821
  if (owner)
4563
4822
  this.scopes[this.scopes.length - 1].push(owner);
4564
4823
  break;
@@ -4652,7 +4911,7 @@ export class LlEmitter {
4652
4911
  b.local &&
4653
4912
  isRefCounted(b.type) &&
4654
4913
  this.stableCallBindings.has(s.localId) &&
4655
- this.canBorrowCallArgument(s.init)) {
4914
+ (this.canBorrowCallArgument(s.init) || this.unwrittenAlias(s))) {
4656
4915
  const v = this.emitReadReceiver(s.init);
4657
4916
  B.line(`store ptr ${v.name}, ptr ${b.slot}`);
4658
4917
  break;
@@ -4719,6 +4978,32 @@ export class LlEmitter {
4719
4978
  }
4720
4979
  B.line(`store ${this.llType(b.type)} ${v.name}, ptr ${b.slot}`);
4721
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
+ }
4722
5007
  break;
4723
5008
  }
4724
5009
  case "exprStmt":
@@ -4816,14 +5101,14 @@ export class LlEmitter {
4816
5101
  const obj = this.emitStableReceiver(s.obj, [s.value]);
4817
5102
  // (The guard precedes the value here: the nullable store
4818
5103
  // evaluates it itself.)
4819
- this.emitPublishedFieldGuard(obj.name, s.className, s.field);
5104
+ this.emitStoreGuard(obj.name, s);
4820
5105
  this.emitNullableFieldStore(obj.name, s.className, s.field, nullable, s.value);
4821
5106
  break;
4822
5107
  }
4823
5108
  if (s.kind === "fieldSet" && this.int32Slots.isField(s.className, s.field)) {
4824
5109
  const obj = this.emitStableReceiver(s.obj, [s.value]);
4825
5110
  const v = this.emitExpr(s.value);
4826
- this.emitPublishedFieldGuard(obj.name, s.className, s.field);
5111
+ this.emitStoreGuard(obj.name, s);
4827
5112
  const { ptr } = this.classFieldPtr(obj.name, s.className, s.field);
4828
5113
  this.storeInt32Field(ptr, v, s.value);
4829
5114
  break;
@@ -4831,7 +5116,7 @@ export class LlEmitter {
4831
5116
  const obj = this.emitStableReceiver(s.obj, [s.value]);
4832
5117
  const v = this.emitExpr(s.value);
4833
5118
  if (s.kind === "fieldSet")
4834
- this.emitPublishedFieldGuard(obj.name, s.className, s.field);
5119
+ this.emitStoreGuard(obj.name, s);
4835
5120
  else if (this.publishedTypes?.recordGuarded(s.shapeId) === true)
4836
5121
  this.emitPublishedFieldGuard(obj.name, null, s.field);
4837
5122
  const { ptr, type } = s.kind === "fieldSet"
@@ -5194,7 +5479,11 @@ export class LlEmitter {
5194
5479
  if (integerArrayLoop && s.init?.kind === "varDecl" && s.init.init) {
5195
5480
  const value = this.emitExpr(s.init.init);
5196
5481
  start = B.tmp();
5197
- 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}`);
5198
5487
  this.integerArrayBindings.add(integerLoop.localId);
5199
5488
  }
5200
5489
  B.line(`store ${this.sizeType} ${start}, ptr ${integerSlot}`);
@@ -5500,10 +5789,32 @@ export class LlEmitter {
5500
5789
  // the actual ret. The parked value owns a synthetic slot-backed
5501
5790
  // scope entry during each copy so a throwing finally releases it.
5502
5791
  let v = null;
5503
- 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) {
5504
5804
  // A borrowed-return body hands back its walk without a reference
5505
- // (it has no try statements, so no finally can intervene).
5506
- v = this.emitReadReceiver(s.value);
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);
5507
5818
  }
5508
5819
  else if (s.value !== null) {
5509
5820
  v = this.emitExpr(s.value);
@@ -5540,7 +5851,7 @@ export class LlEmitter {
5540
5851
  this.finallyStack = savedFinally;
5541
5852
  this.tryStack = savedTry;
5542
5853
  }
5543
- else {
5854
+ else if (moved === null) {
5544
5855
  this.releaseForJump(0, 0);
5545
5856
  }
5546
5857
  if (!B.isTerminated()) {
@@ -5798,7 +6109,7 @@ export class LlEmitter {
5798
6109
  const caseLabels = s.cases.map(() => B.newLabel("sw.c"));
5799
6110
  const defaultIdx = s.cases.findIndex((c) => c.test === null);
5800
6111
  const fallback = defaultIdx >= 0 ? caseLabels[defaultIdx] : end;
5801
- if (!emitLiteralSwitch(this, disc, s.cases, caseLabels, fallback)) {
6112
+ if (!emitLiteralSwitch(this, disc, s.disc, s.cases, caseLabels, fallback)) {
5802
6113
  s.cases.forEach((c, i) => {
5803
6114
  if (c.test === null)
5804
6115
  return;
@@ -6134,6 +6445,13 @@ export class LlEmitter {
6134
6445
  ``,
6135
6446
  ];
6136
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
+ }
6137
6455
  /** The frozen-object guard of a class field or record field store:
6138
6456
  * Node's "Cannot assign to read only property" TypeError when the target
6139
6457
  * is published. `className` null means a record (`#<Object>`). */
@@ -6273,6 +6591,10 @@ export class LlEmitter {
6273
6591
  this.ownSlot(result.owner.slot, result.owner.type);
6274
6592
  return result.value;
6275
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);
6276
6598
  const arrayRead = this.optionalArrayReads.get(e);
6277
6599
  if (arrayRead)
6278
6600
  return emitProjectedArrayRead(this, arrayRead);
@@ -6518,12 +6840,24 @@ export class LlEmitter {
6518
6840
  const wrap = this.borrowableNullableWrap(e);
6519
6841
  return wrap === "unit" || (wrap === "ref" && this.canBorrowReceiver(e.value));
6520
6842
  }
6521
- // A borrowed-return callee preserves edges, so its result stays
6522
- // reachable from borrowable arguments until the consumer runs.
6523
- case "call":
6524
- return (this.borrowedReturns.has(e.callee) &&
6525
- e.args.every((arg) => (!isRefCounted(arg.type) || this.canBorrowReceiver(arg)) &&
6526
- this.referenceEffects.preserves(arg)));
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
+ }
6527
6861
  // A required element read: the (borrowed) array owns the element
6528
6862
  // while an edge-preserving index computes.
6529
6863
  case "arrayGet":
@@ -6569,14 +6903,17 @@ export class LlEmitter {
6569
6903
  };
6570
6904
  const hostFor = (fn) => {
6571
6905
  const allowed = borrowedIds(fn);
6572
- return {
6906
+ const host = {
6573
6907
  pointerLocal: (local) => pointer(local.type),
6574
6908
  borrowsWithoutOwning: (e) => this.borrowsWithoutOwning(e),
6575
6909
  borrowedReturn: (callee) => this.borrowedReturns.has(callee),
6576
6910
  parameterAllowed: (id) => allowed.has(id),
6577
6911
  };
6912
+ return this.referenceEffects.functions.has(fn.name) ? host : this.unwrittenWalkHost(fn, host);
6578
6913
  };
6579
- 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);
6580
6917
  const candidates = preserving.filter((fn) => {
6581
6918
  if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures)
6582
6919
  return false;
@@ -6597,6 +6934,8 @@ export class LlEmitter {
6597
6934
  if (!analyzeWalks(fn, hostFor(fn)).returnsWalk)
6598
6935
  continue;
6599
6936
  this.borrowedReturns.add(fn.name);
6937
+ // Nested borrowed returns extend the locations a caller depends on.
6938
+ this.borrowedReturnKeyCache.clear();
6600
6939
  changed = true;
6601
6940
  }
6602
6941
  }
@@ -6624,6 +6963,12 @@ export class LlEmitter {
6624
6963
  return this.nullableFieldGet(e) === null;
6625
6964
  case "recordGet":
6626
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));
6627
6972
  case "unionNarrow":
6628
6973
  return this.nullableUnions.has(e.unionId);
6629
6974
  case "downcast":