@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
@@ -0,0 +1,1577 @@
1
+ import { isRefCounted, typeKey } from "../ir/ir.js";
2
+ import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
3
+ import { analyzeIntegerRanges, INT32_RANGE, integerArithmeticRange, withinInt32, } from "../ir/integer-ranges.js";
4
+ import { findInitializerBindings, withInitializerBindings } from "../ir/initializer-bindings.js";
5
+ import { unshareNodes } from "../ir/unshare.js";
6
+ import { dynamicallyStoredFamilies, functionCallFacts, Int32Slots } from "../ir/int32-slots.js";
7
+ import { computeMayThrow } from "../backend/may-throw.js";
8
+ import { computeTraced, explainCycle, } from "../backend/cycle-analysis.js";
9
+ import { SourceLocations } from "../backend/source-locations.js";
10
+ import { contractLocKey, } from "../frontend/contract-types.js";
11
+ /** The largest array index: 2^32 - 2 (ECMA-262 array index). */
12
+ const MAX_ARRAY_INDEX = 2 ** 32 - 2;
13
+ /** Walk every expression and statement of a function body, parents first. */
14
+ function walk(body, visit) {
15
+ const expr = (e) => {
16
+ visit.expr?.(e);
17
+ return everyExprChild(e, expr, stmt);
18
+ };
19
+ const stmt = (s) => {
20
+ visit.stmt?.(s);
21
+ return everyStmtChild(s, expr, stmt);
22
+ };
23
+ for (const s of body)
24
+ stmt(s);
25
+ }
26
+ function baseName(file) {
27
+ const slash = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\"));
28
+ return slash >= 0 ? file.slice(slash + 1) : file;
29
+ }
30
+ export function checkContracts(mod, annotations, sourceTexts) {
31
+ // Range proofs are keyed by node identity: check the module emission sees.
32
+ const checker = new ContractChecker(unshareNodes(mod), annotations, sourceTexts);
33
+ return checker.run();
34
+ }
35
+ class ContractChecker {
36
+ mod;
37
+ notes;
38
+ sources;
39
+ diagnostics = [];
40
+ reported = new Set();
41
+ positions;
42
+ functions = new Map();
43
+ classes = new Map();
44
+ fnByLoc = new Map();
45
+ localsByLoc = new Map();
46
+ globalsByLoc = new Map();
47
+ classesByLoc = new Map();
48
+ contracts = {};
49
+ /** Whether any verified fact was recorded. */
50
+ recorded = false;
51
+ constructor(mod, notes, sources) {
52
+ this.mod = mod;
53
+ this.notes = notes;
54
+ this.sources = sources;
55
+ this.positions = new SourceLocations(sources);
56
+ for (const fn of mod.functions) {
57
+ this.functions.set(fn.name, fn);
58
+ // Synthetic dynamic-class helpers (%dyn.*) carry the location of the
59
+ // code that first needed them, which is not their declaration.
60
+ if (!fn.name.startsWith("%dyn."))
61
+ push(this.fnByLoc, contractLocKey(fn.loc), fn);
62
+ for (const local of fn.locals)
63
+ if (local.source)
64
+ push(this.localsByLoc, contractLocKey(local.source.loc), { fn, localId: local.id });
65
+ }
66
+ for (const g of mod.globals ?? [])
67
+ if (g.source)
68
+ push(this.globalsByLoc, contractLocKey(g.source.loc), g.id);
69
+ for (const cls of mod.classes ?? []) {
70
+ this.classes.set(cls.name, cls);
71
+ push(this.classesByLoc, contractLocKey(cls.loc), cls);
72
+ }
73
+ }
74
+ /* ── reporting ── */
75
+ where(loc) {
76
+ if (loc === null)
77
+ return "?";
78
+ const pos = this.positions.position(loc);
79
+ return pos ? `${baseName(pos.file)}:${pos.line}:${pos.column}` : baseName(loc.file);
80
+ }
81
+ text(loc, max = 60) {
82
+ const source = this.sources.get(loc.file);
83
+ if (source === undefined || loc.end <= loc.start)
84
+ return "";
85
+ const raw = source.slice(loc.start, loc.end).replace(/\s+/g, " ").trim();
86
+ return raw.length > max ? `${raw.slice(0, max - 3)}...` : raw;
87
+ }
88
+ report(code, loc, message, hint) {
89
+ const key = `${code}\0${contractLocKey(loc)}\0${message}`;
90
+ if (this.reported.has(key))
91
+ return;
92
+ this.reported.add(key);
93
+ this.diagnostics.push({ code, loc, message, hint });
94
+ }
95
+ displayName(fnName) {
96
+ const fn = this.functions.get(fnName);
97
+ const method = /^%(.*)\.([^.]+)$/.exec(fnName);
98
+ if (method && !fnName.startsWith("%fn") && !fnName.startsWith("%init")) {
99
+ const cls = this.classes.get(method[1]);
100
+ const owner = cls?.jsName ?? method[1];
101
+ return `${owner}.${method[2]}`;
102
+ }
103
+ if (fn?.sourceName !== undefined && fn.sourceName !== "<anonymous>")
104
+ return fn.sourceName;
105
+ if (fn !== undefined && /^%fn\d+$/.test(fnName))
106
+ return `the function at ${this.where(fn.loc)}`;
107
+ return fnName.replace(/^%/, "");
108
+ }
109
+ /** A short TypeScript-like spelling of an IR type, for messages. */
110
+ typeText(t) {
111
+ switch (t.kind) {
112
+ case "f64":
113
+ return "number";
114
+ case "bool":
115
+ return "boolean";
116
+ case "undefinedT":
117
+ return "undefined";
118
+ case "nullT":
119
+ return "null";
120
+ case "object":
121
+ return this.classes.get(t.className)?.jsName ?? t.className.replace(/^%(m\d+\.)?/, "");
122
+ case "union": {
123
+ const def = this.mod.unions?.find((u) => u.id === t.unionId);
124
+ return def ? def.arms.map((a) => this.typeText(a)).join(" | ") : "a union";
125
+ }
126
+ default:
127
+ return t.kind;
128
+ }
129
+ }
130
+ /** Why a throwing parameter access exists although the source type looks
131
+ * safe: the frontend compiles a parameter as `T | undefined` (or null)
132
+ * when a caller can pass a missing value, typically a non-null asserted
133
+ * element read `xs[i]!`, so the TypeError Node raises inside the callee
134
+ * happens there too. Names the parameter and the first such call. */
135
+ widenedParams(fnName) {
136
+ const fn = fnName === undefined ? undefined : this.functions.get(fnName);
137
+ if (fn === undefined)
138
+ return "";
139
+ const nullable = (t) => t.kind === "undefinedT" ||
140
+ t.kind === "nullT" ||
141
+ (t.kind === "union" &&
142
+ this.mod.unions
143
+ ?.find((u) => u.id === t.unionId)
144
+ ?.arms.some((a) => a.kind === "undefinedT" || a.kind === "nullT") === true);
145
+ const params = fn.params.filter((p) => nullable(p.type));
146
+ if (params.length === 0)
147
+ return "";
148
+ const p = params[0];
149
+ const index = fn.params.indexOf(p);
150
+ let site = null;
151
+ for (const caller of this.mod.functions) {
152
+ if (site !== null)
153
+ break;
154
+ walk(caller.body, {
155
+ expr: (e) => {
156
+ if (site !== null || e.kind !== "call" || e.callee !== fn.name)
157
+ return;
158
+ const arg = e.args[index];
159
+ if (arg !== undefined && nullable(arg.type))
160
+ site = arg.loc;
161
+ },
162
+ });
163
+ }
164
+ const name = p.name;
165
+ const because = site !== null ? `: the argument at ${this.where(site)} can be missing` : "";
166
+ return ` (parameter '${name}' is compiled as ${this.typeText(p.type)}${because})`;
167
+ }
168
+ /* ── binding annotations to IR ── */
169
+ bindingsOf(key) {
170
+ const locals = (this.localsByLoc.get(key) ?? []).map((l) => ({ fn: l.fn, id: l.localId }));
171
+ const globals = (this.globalsByLoc.get(key) ?? []).map((id) => ({ fn: null, id }));
172
+ return [...locals, ...globals];
173
+ }
174
+ functionsAt(key) {
175
+ return this.fnByLoc.get(key) ?? [];
176
+ }
177
+ classesAt(key) {
178
+ return this.classesByLoc.get(key) ?? [];
179
+ }
180
+ run() {
181
+ this.checkProgramLifetime();
182
+ this.checkNoThrow();
183
+ this.checkAcyclic();
184
+ const int32 = new Int32Contracts(this);
185
+ int32.run();
186
+ const immutable = new ImmutableContracts(this);
187
+ immutable.run();
188
+ if (this.diagnostics.length > 0)
189
+ return { diagnostics: this.diagnostics, contracts: null };
190
+ const c = this.contracts;
191
+ return { diagnostics: [], contracts: this.recorded ? c : null };
192
+ }
193
+ record(field, values) {
194
+ const list = [...new Set(values)].sort();
195
+ if (list.length === 0)
196
+ return;
197
+ this.contracts[field] = list;
198
+ this.recorded = true;
199
+ }
200
+ /* ── ProgramLifetime ── */
201
+ /** `class C implements ProgramLifetime`: a memory-only promise, so no
202
+ * program behavior depends on it except that instances are never freed.
203
+ * Checked: the class is the root of its hierarchy (every subclass, and so
204
+ * every value of any static type in the hierarchy, is then program-
205
+ * lifetime, which is what lets the backend elide reference counting on
206
+ * those static types); and the build is an executable (library hosts own
207
+ * the lifetimes of the objects they receive). Publication to other
208
+ * threads needs no check: an instance carries a reference count of its
209
+ * own until publication marks it like any other object it reaches
210
+ * (classes.ts programLifetimeRc). Static code has no WeakRef or
211
+ * FinalizationRegistry; weak collections stay correct because an entry
212
+ * keyed by an object that is never freed is never observable as removed. */
213
+ checkProgramLifetime() {
214
+ if (this.notes.classes.size === 0)
215
+ return;
216
+ const roots = [];
217
+ for (const [key, note] of this.notes.classes)
218
+ for (const cls of this.classesAt(key)) {
219
+ if (cls.base !== undefined) {
220
+ const base = this.classes.get(cls.base);
221
+ const baseName = base?.jsName ?? cls.base.replace(/^%(m\d+\.)?/, "");
222
+ this.report("SC7008", note.loc, `'${note.name}' is declared ProgramLifetime but extends '${baseName}'`, base === undefined || base.runtime === true
223
+ ? "ProgramLifetime applies to classes the program owns end to end; built-in bases (errors, emitters, streams) keep runtime state that is released with the object"
224
+ : `declare ProgramLifetime on the root class '${baseName}' instead; it applies to every subclass`);
225
+ continue;
226
+ }
227
+ if (this.mod.lib !== undefined) {
228
+ this.report("SC7008", note.loc, `'${note.name}' is declared ProgramLifetime in a library build`, "a library's host decides when the objects it receives are released; ProgramLifetime is checked in executables only");
229
+ continue;
230
+ }
231
+ roots.push(cls.name);
232
+ }
233
+ if (roots.length === 0)
234
+ return;
235
+ this.record("programLifetime", roots);
236
+ }
237
+ /* ── Acyclic ── */
238
+ checkAcyclic() {
239
+ if (this.notes.acyclic.length === 0)
240
+ return;
241
+ const traced = computeTraced(this.mod);
242
+ const proven = [];
243
+ // Object types are structural in the IR: name a record by the
244
+ // interface or alias that declared it Acyclic.
245
+ for (const { note, unit } of this.notes.acyclic)
246
+ if (unit != null && unit.startsWith("record:"))
247
+ this.unitNames.set(unit, note.name);
248
+ for (const { note, classKey, unit } of this.notes.acyclic) {
249
+ // A class may lower to several IR classes (generic instantiations);
250
+ // an unused type has no unit and no values, so it holds trivially.
251
+ const units = classKey !== undefined
252
+ ? this.classesAt(classKey).map((cls) => `object:${cls.name}`)
253
+ : unit != null
254
+ ? [unit]
255
+ : [];
256
+ for (const key of units) {
257
+ const isTraced = key.startsWith("union:")
258
+ ? traced.unions.has(key.slice("union:".length))
259
+ : traced.shapes.has(key);
260
+ if (!isTraced) {
261
+ proven.push(key);
262
+ continue;
263
+ }
264
+ const why = explainCycle(this.mod, key);
265
+ this.report("SC7007", note.loc, `'${note.name}' is declared Acyclic but can lie on a reference cycle: ${why ? this.cycleText(key, why) : "the cycle analysis traces it"}`, why ? this.cycleHint(why) : "remove Acyclic");
266
+ break;
267
+ }
268
+ }
269
+ this.record("acyclic", proven);
270
+ }
271
+ unitNames = new Map();
272
+ /** A unit key's spelling: a class name, a record's declared name or
273
+ * `{ a, b }`, the arms of a union. */
274
+ unitText(key) {
275
+ const named = this.unitNames.get(key);
276
+ if (named !== undefined)
277
+ return named;
278
+ const colon = key.indexOf(":");
279
+ const kind = key.slice(0, colon), id = key.slice(colon + 1);
280
+ if (kind === "object" || kind === "subtree")
281
+ return this.typeText({ kind: "object", className: id });
282
+ if (kind === "union")
283
+ return this.typeText({ kind: "union", unionId: id });
284
+ const record = this.mod.records?.find((r) => r.id === id);
285
+ return record ? `{ ${record.fields.map((f) => f.name).join(", ")} }` : "an object";
286
+ }
287
+ fieldText(field) {
288
+ if (field === "%dynProperties")
289
+ return "(dynamic properties)";
290
+ if (field === "<listeners>")
291
+ return "(event listeners)";
292
+ if (field === "<class>")
293
+ return "(class object)";
294
+ if (field === "<overflow>")
295
+ return "[key]";
296
+ return field;
297
+ }
298
+ /** The path as `A.f → B.g → A`, folding union and subclass hops into the
299
+ * field that reaches them. */
300
+ stepsText(steps, end) {
301
+ const parts = [];
302
+ // A slot of class type C holding a subclass D reads `D (as a C)`.
303
+ let asBase = null;
304
+ const unit = (key) => {
305
+ const text = this.unitText(key);
306
+ const suffix = asBase !== null && asBase !== text ? ` (as a ${asBase})` : "";
307
+ asBase = null;
308
+ return `${text}${suffix}`;
309
+ };
310
+ for (const step of steps) {
311
+ if (step.label.kind === "subclass") {
312
+ asBase ??= this.unitText(step.from);
313
+ continue;
314
+ }
315
+ if (step.label.kind === "field")
316
+ parts.push(`${unit(step.from)}.${this.fieldText(step.label.field)}`);
317
+ }
318
+ parts.push(unit(end));
319
+ return parts.join(" → ");
320
+ }
321
+ cycleText(key, why) {
322
+ switch (why.kind) {
323
+ case "hierarchy":
324
+ return `its subclass '${this.unitText(why.member)}' shares its object layout, and ${this.cycleText(why.member, why.because)}`;
325
+ case "intrinsic": {
326
+ const holds = why.holds === "func"
327
+ ? "a function"
328
+ : why.holds === "dyn"
329
+ ? "a dynamic value"
330
+ : why.holds === "caught"
331
+ ? "a caught exception"
332
+ : why.holds === "promise"
333
+ ? "a promise"
334
+ : "a class object";
335
+ const field = why.field === "%dynProperties"
336
+ ? `${this.unitText(why.owner)} instances reach dynamic code, whose properties`
337
+ : why.field === ""
338
+ ? this.unitText(why.owner)
339
+ : `${this.unitText(why.owner)}.${this.fieldText(why.field)}`;
340
+ const path = why.steps.length > 0 ? `${this.stepsText(why.steps, why.owner)}, and ` : "";
341
+ return `${path}${field} can hold ${holds}, which can reference any object, including a ${this.unitText(key)}`;
342
+ }
343
+ case "cycle": {
344
+ const mutable = why.steps.find((s) => s.label.kind === "field" && (s.label.mutable || s.label.via !== undefined));
345
+ let reason = "";
346
+ if (mutable) {
347
+ const where = `${this.unitText(mutable.from)}.${this.fieldText(mutable.label.field)}`;
348
+ if (mutable.label.via !== undefined)
349
+ reason = ` (${where} is ${mutable.label.via === "array" ? "an array" : `a ${mutable.label.via === "map" ? "Map" : "Set"}`}, whose contents can change)`;
350
+ else {
351
+ const write = this.writeOutsideConstructor(mutable.from, mutable.label.field);
352
+ reason = ` (${where} can be assigned after construction${write !== null ? `, as at ${this.where(write)}` : ""})`;
353
+ }
354
+ }
355
+ return `${this.stepsText(why.steps, key)}${reason}`;
356
+ }
357
+ }
358
+ }
359
+ cycleHint(why) {
360
+ const inner = why.kind === "hierarchy" ? why.because : why;
361
+ if (inner.kind === "intrinsic")
362
+ return "a function, dynamic or promise field can capture the object itself; store plain data instead, or remove Acyclic";
363
+ if (inner.kind === "cycle" &&
364
+ !inner.steps.some((s) => s.label.kind === "field" && s.label.mutable && s.label.via === undefined))
365
+ return "an array, Map or Set can come to hold an object that refers back; make the element type unable to reach this type, or remove Acyclic";
366
+ return "assign the reference only in the constructor (before `this` escapes), so a new object can only point to older ones, or remove Acyclic";
367
+ }
368
+ /** The first store into the field outside its own class's constructors. */
369
+ writeOutsideConstructor(unit, field) {
370
+ const colon = unit.indexOf(":");
371
+ const kind = unit.slice(0, colon), id = unit.slice(colon + 1);
372
+ let found = null;
373
+ for (const fn of this.mod.functions) {
374
+ if (found !== null)
375
+ break;
376
+ const ctor = /^%(.*)\.constructor$/.exec(fn.name)?.[1];
377
+ const self = ctor !== undefined ? fn.params[0]?.localId : undefined;
378
+ walk(fn.body, {
379
+ stmt: (s) => {
380
+ if (found !== null)
381
+ return;
382
+ if (kind === "record" && s.kind === "recordSet" && s.shapeId === id && s.field === field)
383
+ found = s.loc;
384
+ else if (kind !== "record" &&
385
+ s.kind === "fieldSet" &&
386
+ s.field === field &&
387
+ this.sameHierarchy(s.className, id) &&
388
+ !(self !== undefined && s.obj.kind === "varRef" && s.obj.localId === self))
389
+ found = s.loc;
390
+ },
391
+ });
392
+ }
393
+ return found;
394
+ }
395
+ sameHierarchy(a, b) {
396
+ const root = (name) => {
397
+ let cur = name;
398
+ for (let cls = this.classes.get(cur); cls?.base !== undefined; cls = this.classes.get(cur))
399
+ cur = cls.base;
400
+ return cur;
401
+ };
402
+ return root(a) === root(b);
403
+ }
404
+ /* ── NoThrow ── */
405
+ checkNoThrow() {
406
+ const targets = [];
407
+ for (const [key, list] of this.notes.functions)
408
+ for (const note of list)
409
+ if (note.marker === "NoThrow")
410
+ for (const fn of this.fnByLoc.get(key) ?? [])
411
+ targets.push({ fn, note });
412
+ if (targets.length === 0)
413
+ return;
414
+ // Target-independent: the native stack guard (Linux, macOS) counts, so a
415
+ // contract holds on every platform.
416
+ const mt = computeMayThrow(this.mod, { stackChecks: true, explain: true });
417
+ const proven = [];
418
+ for (const { fn, note } of targets) {
419
+ if (fn.async === true || fn.generator !== undefined) {
420
+ this.report("SC7001", note.loc, `'${note.name}' is declared NoThrow, but async functions and generators report errors through promises and resumption, which the contract does not cover`, "remove NoThrow from async functions and generators");
421
+ continue;
422
+ }
423
+ if (!mt.fns.has(fn.name)) {
424
+ proven.push(fn.name);
425
+ continue;
426
+ }
427
+ const steps = [];
428
+ // The function's own stack guard throws on entry, outside any handler.
429
+ const ownStackGuard = mt.stackChecks?.has(fn.name) === true;
430
+ const seen = new Set();
431
+ let current = fn.name;
432
+ while (current !== undefined && !seen.has(current) && steps.length < 8) {
433
+ seen.add(current);
434
+ const reason = mt.why?.get(current);
435
+ if (reason === undefined)
436
+ break;
437
+ if (reason.kind === "seed") {
438
+ steps.push(`${reason.what} at ${this.where(reason.loc)}${this.widenedParams(current)}`);
439
+ current = undefined;
440
+ }
441
+ else if (reason.kind === "call") {
442
+ steps.push(`calls '${this.displayName(reason.callee)}' at ${this.where(reason.loc)}`);
443
+ current = reason.callee;
444
+ }
445
+ else {
446
+ steps.push(reason.target === ""
447
+ ? `calls a function value at ${this.where(reason.loc)} (dynamic callbacks may throw)`
448
+ : `calls a function value at ${this.where(reason.loc)}, and the function value '${this.displayName(reason.target)}' may throw`);
449
+ current = reason.target === "" ? undefined : reason.target;
450
+ }
451
+ }
452
+ this.report("SC7001", note.loc, `'${note.name}' is declared NoThrow but may throw: ${steps.join(" → ")}`, ownStackGuard
453
+ ? "remove the recursion or remove NoThrow; a recursive function's stack guard throws on entry, outside any try/catch"
454
+ : this.mod.workers === true
455
+ ? "remove the throwing operation from the path (or the recursion), or remove NoThrow; in worker programs try/catch does not discharge the contract, since a termination request unwinds through catch"
456
+ : "remove the throwing operation from the path (or the recursion), catch it with try/catch, or remove NoThrow");
457
+ }
458
+ this.record("noThrow", proven);
459
+ }
460
+ }
461
+ /** The explicit int32 form of a written expression: `x++` and compound
462
+ * assignments become an assignment of the `| 0` result. */
463
+ function int32Rewrite(text) {
464
+ const step = /^(.+?)\s*(\+\+|--)$/.exec(text) ?? /^(\+\+|--)\s*(.+)$/.exec(text);
465
+ if (step) {
466
+ const target = step[1] === "++" || step[1] === "--" ? step[2] : step[1];
467
+ const op = (step[1] === "++" || step[1] === "--" ? step[1] : step[2])[0];
468
+ return `${target} = (${target} ${op} 1) | 0`;
469
+ }
470
+ const compound = /^([^=]+?)\s*(\*\*|>>>|<<|>>|[-+*/%&|^])=\s*(.+)$/.exec(text);
471
+ if (compound)
472
+ return `${compound[1]} = (${compound[1]} ${compound[2]} ${compound[3]}) | 0`;
473
+ const assignment = /^([^=]+?)\s*=\s*([^=].*)$/.exec(text);
474
+ if (assignment)
475
+ return `${assignment[1]} = ${orZero(assignment[2])}`;
476
+ return orZero(text);
477
+ }
478
+ /** `text | 0`, parenthesized unless `text` binds tighter than `|` for sure
479
+ * (a name, member chain, literal or call without nested parentheses). */
480
+ function orZero(text) {
481
+ return /^[\w$.]+(\([^()]*\))?$/.test(text) ? `${text} | 0` : `(${text}) | 0`;
482
+ }
483
+ function push(map, key, value) {
484
+ const list = map.get(key);
485
+ if (list)
486
+ list.push(value);
487
+ else
488
+ map.set(key, [value]);
489
+ }
490
+ /* ─────────────────────────────── Int32 ─────────────────────────────── */
491
+ class Int32Contracts {
492
+ c;
493
+ slots;
494
+ /** Field families, with the note that declared them. */
495
+ fields = new Map();
496
+ params = new Map(); // fn\0localId
497
+ returns = new Map(); // fn
498
+ locals = new Map(); // fn\0localId or \0globalId
499
+ constructor(c) {
500
+ this.c = c;
501
+ this.slots = new Int32Slots(c.classes);
502
+ }
503
+ run() {
504
+ if (!this.bind())
505
+ return;
506
+ if (this.fields.size + this.params.size + this.returns.size + this.locals.size === 0)
507
+ return;
508
+ this.structural();
509
+ this.writes();
510
+ this.c.record("int32Fields", [...this.fields.keys()]);
511
+ this.c.record("int32Params", [...this.params.keys()]);
512
+ this.c.record("int32Returns", [...this.returns.keys()]);
513
+ this.c.record("int32Locals", [...this.locals.keys()]);
514
+ }
515
+ refuse(note, why, hint) {
516
+ this.c.report("SC7003", note.loc, `the Int32 contract on '${note.name}' cannot be checked: ${why}`, hint);
517
+ }
518
+ bind() {
519
+ const c = this.c;
520
+ for (const [key, list] of c.notes.functions)
521
+ for (const note of list) {
522
+ if (note.marker !== "Int32")
523
+ continue;
524
+ for (const fn of c.functionsAt(key)) {
525
+ if (fn.async === true || fn.generator !== undefined) {
526
+ this.refuse(note, "async functions and generators return promises or iterators", "annotate the awaited value where it is stored instead");
527
+ continue;
528
+ }
529
+ if (fn.returnType.kind !== "f64") {
530
+ this.refuse(note, "the result is not a plain number", "write Int32 only on a number result");
531
+ continue;
532
+ }
533
+ this.returns.set(fn.name, note);
534
+ }
535
+ }
536
+ for (const [key, note] of c.notes.bindings) {
537
+ if (note.marker !== "Int32")
538
+ continue;
539
+ for (const b of c.bindingsOf(key)) {
540
+ const type = b.fn
541
+ ? b.fn.locals.find((l) => l.id === b.id)?.type
542
+ : c.mod.globals?.find((g) => g.id === b.id)?.type;
543
+ if (type?.kind !== "f64") {
544
+ this.refuse(note, "the binding is not a plain number (optional and union types are not int32 slots)", "declare it as a required Int32");
545
+ continue;
546
+ }
547
+ if (b.fn === null) {
548
+ this.locals.set(`\0${b.id}`, note);
549
+ continue;
550
+ }
551
+ this.locals.set(`${b.fn.name}\0${b.id}`, note);
552
+ if (b.fn.params.some((p) => p.localId === b.id))
553
+ this.params.set(`${b.fn.name}\0${b.id}`, note);
554
+ }
555
+ }
556
+ for (const [key, note] of c.notes.fields) {
557
+ if (note.marker !== "Int32")
558
+ continue;
559
+ const cut = key.lastIndexOf("\0");
560
+ const field = key.slice(cut + 1);
561
+ for (const cls of c.classesAt(key.slice(0, cut))) {
562
+ const def = cls.fields.find((f) => f.name === field);
563
+ if (!def) {
564
+ this.refuse(note, `the compiled class has no field '${field}'`, "annotate an ordinary instance field");
565
+ continue;
566
+ }
567
+ if (def.type.kind !== "f64") {
568
+ this.refuse(note, "the field is not a plain number", "declare the field as a required Int32");
569
+ continue;
570
+ }
571
+ const family = this.slots.family(cls.name, field);
572
+ if (family !== null)
573
+ this.fields.set(family, note);
574
+ }
575
+ }
576
+ // Captured bindings: every capture of an Int32 local shares its box.
577
+ this.propagateCaptures();
578
+ return true;
579
+ }
580
+ propagateCaptures() {
581
+ const c = this.c;
582
+ for (let changed = true; changed;) {
583
+ changed = false;
584
+ for (const fn of c.mod.functions)
585
+ walk(fn.body, {
586
+ expr: (e) => {
587
+ if (e.kind !== "closure")
588
+ return;
589
+ const lifted = c.functions.get(e.fnName);
590
+ e.captures.forEach((id, i) => {
591
+ const inner = lifted?.captures?.[i];
592
+ if (!inner)
593
+ return;
594
+ const outerKey = `${fn.name}\0${id}`;
595
+ const innerKey = `${lifted.name}\0${inner.localId}`;
596
+ const outer = this.locals.get(outerKey);
597
+ const innerNote = this.locals.get(innerKey);
598
+ if (outer && !innerNote) {
599
+ this.locals.set(innerKey, outer);
600
+ changed = true;
601
+ }
602
+ else if (innerNote && !outer) {
603
+ this.locals.set(outerKey, innerNote);
604
+ changed = true;
605
+ }
606
+ });
607
+ },
608
+ });
609
+ }
610
+ }
611
+ /** Slots whose writes the IR does not show. */
612
+ structural() {
613
+ const c = this.c;
614
+ const mod = c.mod;
615
+ if (this.fields.size > 0) {
616
+ const dynamic = dynamicallyStoredFamilies(mod, this.slots);
617
+ for (const [family, note] of this.fields) {
618
+ const owner = family.slice(0, family.indexOf("\0"));
619
+ let generic = false;
620
+ for (let cls = c.classes.get(owner); cls; cls = cls.base ? c.classes.get(cls.base) : undefined)
621
+ if (cls.runtime === true ||
622
+ cls.genericOf !== undefined ||
623
+ (cls.base && !c.classes.has(cls.base)))
624
+ generic = true;
625
+ for (const cls of c.classes.values())
626
+ if (cls.genericOf !== undefined && (cls.genericOf === owner || cls.name === owner))
627
+ generic = true;
628
+ if (generic)
629
+ this.refuse(note, "the class is generic or extends a runtime-provided class", "move the field to a non-generic class");
630
+ else if (dynamic.has(family))
631
+ this.refuse(note, "instances of the class reach dynamic code (any, unknown casts, islands) where a store to the field is not checked", "keep instances of this class out of any-typed values, or remove Int32");
632
+ }
633
+ }
634
+ if (this.params.size > 0) {
635
+ const children = new Map();
636
+ for (const cls of c.classes.values())
637
+ if (cls.base)
638
+ push(children, cls.base, cls.name);
639
+ const subtree = (name) => {
640
+ const out = [];
641
+ const stack = [name];
642
+ while (stack.length) {
643
+ const next = stack.pop();
644
+ out.push(next);
645
+ for (const child of children.get(next) ?? [])
646
+ stack.push(child);
647
+ }
648
+ return out;
649
+ };
650
+ const calls = functionCallFacts(mod, c.classes, subtree);
651
+ for (const [key, note] of this.params) {
652
+ const fnName = key.slice(0, key.indexOf("\0"));
653
+ if (calls.valueEscaped.has(fnName))
654
+ this.refuse(note, `'${c.displayName(fnName)}' is used as a value (callback, class value or export), so its callers are not all visible`, "wrap it in a function that takes a number and passes `x | 0`");
655
+ else if (calls.runtimeVirtual(fnName))
656
+ this.refuse(note, "the runtime calls this method", "remove Int32 from runtime-dispatched methods");
657
+ }
658
+ }
659
+ }
660
+ int32Facts(fnName) {
661
+ const params = new Map();
662
+ for (const key of this.params.keys())
663
+ if (key.startsWith(`${fnName}\0`))
664
+ params.set(key.slice(fnName.length + 1), INT32_RANGE);
665
+ return {
666
+ params,
667
+ field: (className, field) => {
668
+ const family = this.slots.family(className, field);
669
+ return family !== null && this.fields.has(family) ? INT32_RANGE : null;
670
+ },
671
+ call: (callee) => (this.returns.has(callee) ? INT32_RANGE : null),
672
+ boxed: (localId) => (this.locals.has(`${fnName}\0${localId}`) ? INT32_RANGE : null),
673
+ locals: (localId) => this.locals.has(`${fnName}\0${localId}`) || this.locals.has(`\0${localId}`)
674
+ ? INT32_RANGE
675
+ : null,
676
+ };
677
+ }
678
+ writes() {
679
+ const c = this.c;
680
+ const methodImpls = new Map();
681
+ for (const cls of c.classes.values())
682
+ for (const m of cls.methods ?? [])
683
+ push(methodImpls, m, `%${cls.name}.${m}`);
684
+ const bindings = findInitializerBindings(c.mod);
685
+ const fail = (note, value, at, what) => {
686
+ const shown = value ? c.text(value.loc) : "";
687
+ c.report("SC7002", at, `${what} '${note.name}' (Int32) is not provably an int32${shown ? `: '${shown}'` : ""}`, shown
688
+ ? `make the int32 conversion explicit, for example '${int32Rewrite(shown)}', so Node computes the same value`
689
+ : "make the int32 conversion explicit with '| 0', so Node computes the same value");
690
+ };
691
+ for (const fn of c.mod.functions) {
692
+ const numeric = withInitializerBindings(fn, bindings.get(fn.name) ?? []);
693
+ const ranges = analyzeIntegerRanges(numeric, this.int32Facts(fn.name));
694
+ const int32 = (e) => e.type.kind === "f64" && withinInt32(ranges.get(e));
695
+ const localNote = (id) => this.locals.get(`${fn.name}\0${id}`) ?? this.locals.get(`\0${id}`);
696
+ const checkArgs = (callee, args, offset, at) => {
697
+ const target = c.functions.get(callee);
698
+ if (!target)
699
+ return;
700
+ target.params.forEach((param, i) => {
701
+ const note = this.params.get(`${callee}\0${param.localId}`);
702
+ if (!note)
703
+ return;
704
+ const arg = args[i - offset];
705
+ if (i < offset)
706
+ return;
707
+ if (arg === undefined)
708
+ fail(note, null, at, "a call passes no argument for");
709
+ else if (!int32(arg))
710
+ fail(note, arg, arg.loc, "the argument for");
711
+ });
712
+ };
713
+ const incDecStored = (e, prefix, op) => {
714
+ const result = ranges.get(e);
715
+ if (!result)
716
+ return false;
717
+ if (prefix)
718
+ return withinInt32(result);
719
+ return withinInt32(integerArithmeticRange(op, result, { min: 1, max: 1 }));
720
+ };
721
+ walk(fn.body, {
722
+ stmt: (s) => {
723
+ if (s.kind === "varDecl" || s.kind === "assign") {
724
+ const note = localNote(s.localId);
725
+ const value = s.kind === "varDecl" ? s.init : s.value;
726
+ if (note && value && !int32(value))
727
+ fail(note, value, value.loc, "the value stored into");
728
+ }
729
+ else if (s.kind === "forOf" ||
730
+ (s.kind === "tryCatch" && s.catchLocalId !== undefined)) {
731
+ const id = s.kind === "forOf" ? s.localId : s.catchLocalId;
732
+ const note = localNote(id);
733
+ if (note)
734
+ fail(note, null, s.loc, s.kind === "forOf"
735
+ ? "a for-of loop element stored into"
736
+ : "a caught value stored into");
737
+ }
738
+ else if (s.kind === "fieldSet") {
739
+ const family = this.slots.family(s.className, s.field);
740
+ const note = family !== null ? this.fields.get(family) : undefined;
741
+ if (note && !int32(s.value))
742
+ fail(note, s.value, s.value.loc, "the value stored into");
743
+ }
744
+ else if (s.kind === "return") {
745
+ const note = this.returns.get(fn.name);
746
+ if (note && (!s.value || !int32(s.value)))
747
+ fail(note, s.value, s.value?.loc ?? s.loc, "the value returned from");
748
+ }
749
+ },
750
+ expr: (e) => {
751
+ if (e.kind === "assignExpr") {
752
+ const note = localNote(e.localId);
753
+ if (note && !int32(e.value))
754
+ fail(note, e.value, e.value.loc, "the value stored into");
755
+ }
756
+ else if (e.kind === "incDec") {
757
+ const note = localNote(e.localId);
758
+ if (note && !incDecStored(e, e.prefix, e.op))
759
+ c.report("SC7002", e.loc, `'${c.text(e.loc)}' can step '${note.name}' (Int32) outside the int32 range`, `bound the value by an Int32 comparison in the loop condition, or write '${note.name} = (${note.name} ${e.op} 1) | 0'`);
760
+ }
761
+ else if (e.kind === "fieldIncDec") {
762
+ const family = this.slots.family(e.className, e.field);
763
+ const note = family !== null ? this.fields.get(family) : undefined;
764
+ if (note) {
765
+ const target = c.text(e.obj.loc);
766
+ const field = `${target}.${e.field}`;
767
+ c.report("SC7002", e.loc, `'${c.text(e.loc)}' can step the Int32 field '${note.name}' outside the int32 range`, `write '${field} = (${field} ${e.op} 1) | 0'`);
768
+ }
769
+ }
770
+ else if (e.kind === "call")
771
+ checkArgs(e.callee, e.args, 0, e.loc);
772
+ else if (e.kind === "new")
773
+ checkArgs(`%${e.className}.constructor`, e.args, 1, e.loc);
774
+ else if (e.kind === "virtualCall")
775
+ for (const impl of methodImpls.get(e.method) ?? [])
776
+ checkArgs(impl, e.args, 0, e.loc);
777
+ },
778
+ });
779
+ }
780
+ }
781
+ }
782
+ /* ───────────────────────────── Immutable ───────────────────────────── */
783
+ /** Read-only and searching array intrinsics: the receiver is not written,
784
+ * and arguments are compared or copied out, never stored. */
785
+ const ARR_READS = new Set([
786
+ "length",
787
+ "getNumber",
788
+ "indexEq",
789
+ "indexOf",
790
+ "includes",
791
+ "nextPresent",
792
+ "join",
793
+ "slice",
794
+ "flatCopy",
795
+ "flatOne",
796
+ "toReversed",
797
+ "toSpliced",
798
+ "toSortedPrimitive",
799
+ "concatSpread",
800
+ ]);
801
+ const MAP_READS = new Set([
802
+ "get",
803
+ "has",
804
+ "size",
805
+ "iterCount",
806
+ "iterLive",
807
+ "iterKey",
808
+ "iterValue",
809
+ "iterEnter",
810
+ "iterExit",
811
+ "clone",
812
+ "keySet",
813
+ "valueSet",
814
+ ]);
815
+ const SET_READS = new Set([
816
+ "has",
817
+ "size",
818
+ "iterCount",
819
+ "iterLive",
820
+ "iterKey",
821
+ "iterEnter",
822
+ "iterExit",
823
+ "clone",
824
+ ]);
825
+ /** Element reads whose result can be part of the receiver's graph. */
826
+ const MAP_ELEMENT_READS = new Set(["get", "iterKey", "iterValue"]);
827
+ const SET_ELEMENT_READS = new Set(["iterKey"]);
828
+ /** Copies: a fresh container holding the receiver's elements. */
829
+ const ARR_COPIES = new Set([
830
+ "slice",
831
+ "flatCopy",
832
+ "flatOne",
833
+ "toReversed",
834
+ "toSpliced",
835
+ "concatSpread",
836
+ "toSortedPrimitive",
837
+ ]);
838
+ const COLLECTION_COPIES = new Set(["clone", "keySet", "valueSet"]);
839
+ /** Expression kinds whose result is the same value as their child. */
840
+ const PASS_THROUGH = new Set([
841
+ "upcast",
842
+ "downcast",
843
+ "unionWrap",
844
+ "unionNarrow",
845
+ "ternary",
846
+ "logical",
847
+ "nullish",
848
+ "orDefault",
849
+ "seqExpr",
850
+ "chainRecv",
851
+ "optChain",
852
+ "assignExpr",
853
+ ]);
854
+ /** Observations: the value is inspected, never stored or written. */
855
+ const OBSERVE = new Set([
856
+ "toBool",
857
+ "strEq",
858
+ "unionEq",
859
+ "unionFuncEq",
860
+ "unionIsTag",
861
+ "unionDisc",
862
+ "instanceOf",
863
+ "instanceOfValue",
864
+ "dynScalarEq",
865
+ "fieldAbsent",
866
+ "recordHas",
867
+ "recordOvfHas",
868
+ "arrayHas",
869
+ "arrayState",
870
+ "jsonStringify",
871
+ "toString",
872
+ "strConcat",
873
+ "bin",
874
+ "unary",
875
+ "unionKeyGet",
876
+ "recordKeyGet",
877
+ "recordOvfKeys",
878
+ ]);
879
+ /** Library calls that only read their arguments (console output). */
880
+ const LIB_READS = /^(console\.|inspect\.|json\.stringify)/;
881
+ class ImmutableContracts {
882
+ c;
883
+ /** Annotated slots: storing into them requires a fresh or immutable value. */
884
+ annotated = new Map();
885
+ /** Slots that may hold an immutable value (annotated plus inferred). */
886
+ mayHold = new Set();
887
+ /** The first store that made an inferred slot hold an immutable value. */
888
+ via = new Map();
889
+ /** Functions whose results are fresh (factory functions). */
890
+ freshFns = new Set();
891
+ methodImpls = new Map();
892
+ slots;
893
+ /** freshLocalsOf results, valid once the factory set is final. */
894
+ freshLocalsCache = new Map();
895
+ factoriesFinal = false;
896
+ constructor(c) {
897
+ this.c = c;
898
+ this.slots = new Int32Slots(c.classes);
899
+ for (const cls of c.classes.values())
900
+ for (const m of cls.methods ?? [])
901
+ push(this.methodImpls, m, `%${cls.name}.${m}`);
902
+ }
903
+ refuse(note, why, hint) {
904
+ this.c.report("SC7005", note.loc, `the Immutable contract on '${note.name}' cannot be checked: ${why}`, hint);
905
+ }
906
+ run() {
907
+ this.bind();
908
+ if (this.annotated.size === 0)
909
+ return;
910
+ for (const key of this.annotated.keys())
911
+ this.mayHold.add(key);
912
+ this.computeFreshFunctions();
913
+ // Grow the may-hold set to a fixpoint, then report from the final pass.
914
+ for (let round = 0; round < 64; round++) {
915
+ const before = this.mayHold.size;
916
+ this.pass(false);
917
+ if (this.mayHold.size === before)
918
+ break;
919
+ }
920
+ this.pass(true);
921
+ this.immortalGlobals();
922
+ }
923
+ bind() {
924
+ const c = this.c;
925
+ for (const [key, note] of c.notes.bindings) {
926
+ if (note.marker !== "Immutable")
927
+ continue;
928
+ for (const b of c.bindingsOf(key))
929
+ this.annotated.set(b.fn ? `l:${b.fn.name}\0${b.id}` : `g:${b.id}`, note);
930
+ }
931
+ for (const [key, list] of c.notes.functions)
932
+ for (const note of list)
933
+ if (note.marker === "Immutable")
934
+ for (const fn of c.functionsAt(key))
935
+ this.annotated.set(`r:${fn.name}`, note);
936
+ for (const [key, note] of c.notes.fields) {
937
+ if (note.marker !== "Immutable")
938
+ continue;
939
+ const cut = key.lastIndexOf("\0");
940
+ const field = key.slice(cut + 1);
941
+ for (const cls of c.classesAt(key.slice(0, cut))) {
942
+ const family = this.slots.family(cls.name, field);
943
+ if (family === null)
944
+ this.refuse(note, `the compiled class has no field '${field}'`, "annotate an ordinary instance field");
945
+ else
946
+ this.annotated.set(`f:${family}`, note);
947
+ }
948
+ }
949
+ }
950
+ localSlot(fn, id) {
951
+ return id.startsWith("%g.") ? `g:${id}` : `l:${fn.name}\0${id}`;
952
+ }
953
+ fieldSlot(className, field) {
954
+ const family = this.slots.family(className, field);
955
+ return family === null ? null : `f:${family}`;
956
+ }
957
+ /** A factory: every return is fresh, built from literals and locals that
958
+ * only it references. */
959
+ computeFreshFunctions() {
960
+ const c = this.c;
961
+ for (let changed = true; changed;) {
962
+ changed = false;
963
+ for (const fn of c.mod.functions) {
964
+ if (this.freshFns.has(fn.name) || fn.async || fn.generator || fn.captures?.length)
965
+ continue;
966
+ if (!isRefCounted(fn.returnType) || fn.returnType.kind === "string")
967
+ continue;
968
+ const freshLocals = this.freshLocalsOf(fn);
969
+ let returns = 0;
970
+ let ok = true;
971
+ walk(fn.body, {
972
+ stmt: (s) => {
973
+ if (s.kind !== "return")
974
+ return;
975
+ returns++;
976
+ if (!s.value || !this.fresh(s.value, freshLocals))
977
+ ok = false;
978
+ },
979
+ });
980
+ if (ok && returns > 0) {
981
+ this.freshFns.add(fn.name);
982
+ changed = true;
983
+ }
984
+ }
985
+ }
986
+ this.factoriesFinal = true;
987
+ }
988
+ /** Locals of `fn` that only ever hold fresh values and are used only as
989
+ * intrinsic receivers, field-write targets, element writes and returns:
990
+ * the value leaves the function only by `return`. */
991
+ freshLocalsOf(fn) {
992
+ const cached = this.factoriesFinal ? this.freshLocalsCache.get(fn.name) : undefined;
993
+ if (cached !== undefined)
994
+ return cached;
995
+ const result = this.computeFreshLocals(fn);
996
+ if (this.factoriesFinal)
997
+ this.freshLocalsCache.set(fn.name, result);
998
+ return result;
999
+ }
1000
+ computeFreshLocals(fn) {
1001
+ const candidates = new Set(fn.locals.filter((l) => !l.boxed && isRefCounted(l.type)).map((l) => l.id));
1002
+ for (const p of fn.params)
1003
+ candidates.delete(p.localId);
1004
+ const allowed = new Set();
1005
+ let ranges;
1006
+ // An element write at a negative, fractional or too-large index adds an
1007
+ // ordinary property instead, and publication refuses arrays that carry
1008
+ // properties: only writes at provable array indexes keep a value fresh.
1009
+ // Typed-array writes never add properties.
1010
+ const arrayIndex = (index) => {
1011
+ ranges ??= analyzeIntegerRanges(fn);
1012
+ const range = ranges.get(index);
1013
+ return (range !== null && range !== undefined && range.min >= 0 && range.max <= MAX_ARRAY_INDEX);
1014
+ };
1015
+ walk(fn.body, {
1016
+ stmt: (s) => {
1017
+ if (s.kind === "return" && s.value?.kind === "varRef")
1018
+ allowed.add(s.value);
1019
+ if ((s.kind === "fieldSet" || s.kind === "recordSet") && s.obj.kind === "varRef")
1020
+ allowed.add(s.obj);
1021
+ if (s.kind === "bytesSet" && s.arr.kind === "varRef")
1022
+ allowed.add(s.arr);
1023
+ if (s.kind === "arraySet" && s.arr.kind === "varRef" && arrayIndex(s.index))
1024
+ allowed.add(s.arr);
1025
+ },
1026
+ expr: (e) => {
1027
+ if ((e.kind === "arrIntrinsic" || e.kind === "mapIntrinsic" || e.kind === "setIntrinsic") &&
1028
+ e.receiver.kind === "varRef")
1029
+ allowed.add(e.receiver);
1030
+ if ((e.kind === "arrayGet" || e.kind === "arrayHas") && e.arr.kind === "varRef")
1031
+ allowed.add(e.arr);
1032
+ if ((e.kind === "fieldGet" || e.kind === "recordGet") && e.obj.kind === "varRef")
1033
+ allowed.add(e.obj);
1034
+ },
1035
+ });
1036
+ for (let changed = true; changed;) {
1037
+ changed = false;
1038
+ walk(fn.body, {
1039
+ stmt: (s) => {
1040
+ const value = s.kind === "varDecl" ? s.init : s.kind === "assign" ? s.value : undefined;
1041
+ const target = s.kind === "varDecl" || s.kind === "assign" ? s.localId : "";
1042
+ if (value && candidates.has(target) && !this.fresh(value, candidates)) {
1043
+ candidates.delete(target);
1044
+ changed = true;
1045
+ }
1046
+ if (s.kind === "forOf" && candidates.delete(s.localId))
1047
+ changed = true;
1048
+ },
1049
+ expr: (e) => {
1050
+ if (e.kind === "varRef" && candidates.has(e.localId) && !allowed.has(e)) {
1051
+ candidates.delete(e.localId);
1052
+ changed = true;
1053
+ }
1054
+ if ((e.kind === "assignExpr" || e.kind === "incDec") && candidates.delete(e.localId))
1055
+ changed = true;
1056
+ },
1057
+ });
1058
+ }
1059
+ return candidates;
1060
+ }
1061
+ /** A value nothing else references: a literal, a fresh construction or
1062
+ * factory result, a primitive, or a value that is already immutable. */
1063
+ fresh(e, freshLocals, derived) {
1064
+ if (!isRefCounted(e.type) || e.type.kind === "string")
1065
+ return true;
1066
+ if (derived?.(e))
1067
+ return true;
1068
+ switch (e.kind) {
1069
+ case "strLit":
1070
+ case "unitLit":
1071
+ return true;
1072
+ case "varRef":
1073
+ return freshLocals.has(e.localId);
1074
+ case "arrayLit":
1075
+ return e.elems.every((x) => this.fresh(x, freshLocals, derived));
1076
+ case "recordLit":
1077
+ return e.fields.every((f) => this.fresh(f.value, freshLocals, derived));
1078
+ case "mapNew":
1079
+ return (e.seed ?? []).every((p) => this.fresh(p.key, freshLocals, derived) && this.fresh(p.value, freshLocals, derived));
1080
+ case "setNew":
1081
+ return e.seed === undefined || this.fresh(e.seed, freshLocals, derived);
1082
+ case "new":
1083
+ return (this.constructorKeepsThis(e.className) &&
1084
+ e.args.every((a) => this.fresh(a, freshLocals, derived)));
1085
+ case "call":
1086
+ return (this.freshFns.has(e.callee) && e.args.every((a) => this.fresh(a, freshLocals, derived)));
1087
+ case "upcast":
1088
+ case "downcast":
1089
+ case "unionWrap":
1090
+ case "unionNarrow":
1091
+ return this.fresh(e.value, freshLocals, derived);
1092
+ case "ternary":
1093
+ return (this.fresh(e.then, freshLocals, derived) && this.fresh(e.else_, freshLocals, derived));
1094
+ default:
1095
+ return false;
1096
+ }
1097
+ }
1098
+ /** The constructor chain uses `this` only to read and write its own
1099
+ * fields: the new object is referenced from nowhere else. */
1100
+ constructorKeepsThis(className) {
1101
+ const c = this.c;
1102
+ for (let cls = c.classes.get(className); cls; cls = cls.base ? c.classes.get(cls.base) : undefined) {
1103
+ if (cls.runtime === true)
1104
+ return false;
1105
+ const ctor = c.functions.get(`%${cls.name}.constructor`);
1106
+ if (!ctor)
1107
+ continue;
1108
+ const self = ctor.params[0]?.localId;
1109
+ if (self === undefined)
1110
+ return false;
1111
+ const allowed = new Set();
1112
+ walk(ctor.body, {
1113
+ stmt: (s) => {
1114
+ if (s.kind === "fieldSet" && s.obj.kind === "varRef")
1115
+ allowed.add(s.obj);
1116
+ },
1117
+ expr: (e) => {
1118
+ if (e.kind === "fieldGet" && e.obj.kind === "varRef")
1119
+ allowed.add(e.obj);
1120
+ if (e.kind === "call" &&
1121
+ e.callee === `%${cls.base}.constructor` &&
1122
+ e.args[0]?.kind === "varRef")
1123
+ allowed.add(e.args[0]);
1124
+ },
1125
+ });
1126
+ let ok = true;
1127
+ walk(ctor.body, {
1128
+ expr: (e) => {
1129
+ if (e.kind === "varRef" && e.localId === self && !allowed.has(e))
1130
+ ok = false;
1131
+ if (e.kind === "closure" && e.captures.includes(self))
1132
+ ok = false;
1133
+ },
1134
+ });
1135
+ if (!ok)
1136
+ return false;
1137
+ }
1138
+ return true;
1139
+ }
1140
+ /** One pass over the program: derived values, stores, escapes and
1141
+ * mutations. With `report`, violations become diagnostics. */
1142
+ pass(report) {
1143
+ const c = this.c;
1144
+ for (const fn of c.mod.functions) {
1145
+ const derivedMemo = new Map();
1146
+ const freshLocals = this.freshLocalsOf(fn);
1147
+ const derived = (e) => {
1148
+ if (!isRefCounted(e.type) || e.type.kind === "string")
1149
+ return false;
1150
+ const memo = derivedMemo.get(e);
1151
+ if (memo !== undefined)
1152
+ return memo;
1153
+ derivedMemo.set(e, false);
1154
+ const result = this.derivedExpr(fn, e, derived);
1155
+ derivedMemo.set(e, result);
1156
+ return result;
1157
+ };
1158
+ const hold = (slot, value, at) => {
1159
+ if (slot === null || !derived(value))
1160
+ return;
1161
+ if (!this.mayHold.has(slot)) {
1162
+ this.mayHold.add(slot);
1163
+ this.via.set(slot, at);
1164
+ }
1165
+ };
1166
+ const store = (slot, value, how) => {
1167
+ if (slot === null)
1168
+ return;
1169
+ const note = this.annotated.get(slot);
1170
+ if (note && report && !this.fresh(value, freshLocals, derived)) {
1171
+ c.report("SC7004", value.loc, `the value stored into '${note.name}' (Immutable) may be referenced elsewhere: '${c.text(value.loc)}' is not a fresh literal, construction or factory result`, "build the value in place (a literal, `new`, or a function that returns what it builds), so no mutable reference to it remains");
1172
+ }
1173
+ hold(slot, value, how ?? `stored at ${c.where(value.loc)}`);
1174
+ };
1175
+ const mutate = (target, at, what) => {
1176
+ if (!report || !derived(target))
1177
+ return;
1178
+ const shown = c.text(target.loc);
1179
+ const origin = this.originOf(fn, target);
1180
+ const subject = origin === "" && shown !== "" && shown !== c.text(at) ? ` ('${shown}')` : "";
1181
+ c.report("SC7004", at, `${what} writes to a value that may be Immutable${subject}${origin}`, "Immutable values cannot be written through any reference; copy the value before changing it");
1182
+ };
1183
+ const escape = (value, at, what) => {
1184
+ if (!report || !derived(value))
1185
+ return;
1186
+ c.report("SC7005", at, `a value that may be Immutable escapes to ${what}, where writes are not checked: '${c.text(value.loc)}'${this.originOf(fn, value)}`, "keep Immutable values in typed code, or copy the value before handing it out");
1187
+ };
1188
+ const args = (callee, list, offset, at) => {
1189
+ const target = c.functions.get(callee);
1190
+ list.forEach((arg, i) => {
1191
+ const param = target?.params[i + offset];
1192
+ if (!target || !param)
1193
+ return escape(arg, arg.loc, `a call to '${c.displayName(callee)}'`);
1194
+ store(`l:${callee}\0${param.localId}`, arg, `passed to '${c.displayName(callee)}' at ${c.where(at)}`);
1195
+ });
1196
+ };
1197
+ walk(fn.body, {
1198
+ stmt: (s) => {
1199
+ switch (s.kind) {
1200
+ case "varDecl":
1201
+ if (s.init)
1202
+ store(this.localSlot(fn, s.localId), s.init);
1203
+ break;
1204
+ case "assign":
1205
+ store(this.localSlot(fn, s.localId), s.value);
1206
+ break;
1207
+ case "return":
1208
+ if (s.value)
1209
+ store(`r:${fn.name}`, s.value, `returned from '${c.displayName(fn.name)}' at ${c.where(s.loc)}`);
1210
+ break;
1211
+ case "fieldSet":
1212
+ mutate(s.obj, s.loc, `'${c.text(s.loc)}'`);
1213
+ store(this.fieldSlot(s.className, s.field), s.value);
1214
+ break;
1215
+ case "recordSet":
1216
+ mutate(s.obj, s.loc, `'${c.text(s.loc)}'`);
1217
+ store(`s:${s.shapeId}\0${s.field}`, s.value);
1218
+ break;
1219
+ case "recordKeySet":
1220
+ mutate(s.obj, s.loc, `'${c.text(s.loc)}'`);
1221
+ escape(s.value, s.loc, "a dynamically keyed property");
1222
+ break;
1223
+ case "recordKeyDelete":
1224
+ mutate(s.obj, s.loc, `'${c.text(s.loc)}'`);
1225
+ break;
1226
+ case "arraySet":
1227
+ mutate(s.arr, s.loc, `'${c.text(s.loc)}'`);
1228
+ store(`e:${typeKey(s.arr.type)}`, s.value);
1229
+ break;
1230
+ case "arraySetLength":
1231
+ case "arraySetUndefined":
1232
+ case "arrayDelete":
1233
+ case "bytesSet":
1234
+ mutate(s.arr, s.loc, `'${c.text(s.loc)}'`);
1235
+ break;
1236
+ case "forOf": {
1237
+ const slot = this.localSlot(fn, s.localId);
1238
+ const elems = this.mayHold.has(`e:${typeKey(s.iterable.type)}`) ||
1239
+ this.mayHold.has(`se:${typeKey(s.iterable.type)}`);
1240
+ if ((elems || derived(s.iterable)) && !this.mayHold.has(slot)) {
1241
+ this.mayHold.add(slot);
1242
+ this.via.set(slot, `iterated at ${c.where(s.loc)}`);
1243
+ }
1244
+ break;
1245
+ }
1246
+ case "throw":
1247
+ escape(s.value, s.loc, "a throw");
1248
+ break;
1249
+ }
1250
+ },
1251
+ expr: (e) => {
1252
+ switch (e.kind) {
1253
+ case "assignExpr":
1254
+ store(this.localSlot(fn, e.localId), e.value);
1255
+ return;
1256
+ case "call":
1257
+ return args(e.callee, e.args, 0, e.loc);
1258
+ case "new":
1259
+ return args(`%${e.className}.constructor`, e.args, 1, e.loc);
1260
+ case "virtualCall":
1261
+ for (const impl of this.methodImpls.get(e.method) ?? [])
1262
+ args(impl, e.args, 0, e.loc);
1263
+ return;
1264
+ case "fieldIncDec":
1265
+ return mutate(e.obj, e.loc, `'${c.text(e.loc)}'`);
1266
+ case "arrIntrinsic":
1267
+ if (ARR_COPIES.has(e.method))
1268
+ this.copyElements(e.receiver, e.type, derived(e.receiver), c.where(e.loc));
1269
+ if (!ARR_READS.has(e.method))
1270
+ mutate(e.receiver, e.loc, `'${c.text(e.loc)}'`);
1271
+ for (const a of e.args) {
1272
+ if (e.method === "indexOf" || e.method === "includes" || e.method === "indexEq")
1273
+ continue;
1274
+ store(`e:${typeKey(e.receiver.type)}`, a);
1275
+ }
1276
+ return;
1277
+ case "mapIntrinsic":
1278
+ if (COLLECTION_COPIES.has(e.method))
1279
+ this.copyElements(e.receiver, e.type, derived(e.receiver), c.where(e.loc));
1280
+ if (!MAP_READS.has(e.method))
1281
+ mutate(e.receiver, e.loc, `'${c.text(e.loc)}'`);
1282
+ if (e.method === "set") {
1283
+ if (e.args[0])
1284
+ store(`mk:${typeKey(e.receiver.type)}`, e.args[0]);
1285
+ if (e.args[1])
1286
+ store(`mv:${typeKey(e.receiver.type)}`, e.args[1]);
1287
+ }
1288
+ return;
1289
+ case "setIntrinsic":
1290
+ if (COLLECTION_COPIES.has(e.method))
1291
+ this.copyElements(e.receiver, e.type, derived(e.receiver), c.where(e.loc));
1292
+ if (!SET_READS.has(e.method))
1293
+ mutate(e.receiver, e.loc, `'${c.text(e.loc)}'`);
1294
+ if (e.method === "add" && e.args[0])
1295
+ store(`se:${typeKey(e.receiver.type)}`, e.args[0]);
1296
+ return;
1297
+ case "arrayLit":
1298
+ for (const x of e.elems)
1299
+ store(`e:${typeKey(e.type)}`, x);
1300
+ return;
1301
+ case "recordLit":
1302
+ if (e.type.kind === "record")
1303
+ for (const f of e.fields)
1304
+ store(`s:${e.type.shapeId}\0${f.name}`, f.value);
1305
+ return;
1306
+ case "mapNew":
1307
+ for (const p of e.seed ?? []) {
1308
+ store(`mk:${typeKey(e.type)}`, p.key);
1309
+ store(`mv:${typeKey(e.type)}`, p.value);
1310
+ }
1311
+ return;
1312
+ case "closure": {
1313
+ const lifted = c.functions.get(e.fnName);
1314
+ e.captures.forEach((id, i) => {
1315
+ const inner = lifted?.captures?.[i];
1316
+ const outer = this.localSlot(fn, id);
1317
+ if (!inner)
1318
+ return;
1319
+ const innerSlot = `l:${lifted.name}\0${inner.localId}`;
1320
+ if (this.mayHold.has(outer) && !this.mayHold.has(innerSlot)) {
1321
+ this.mayHold.add(innerSlot);
1322
+ this.via.set(innerSlot, `captured at ${c.where(e.loc)}`);
1323
+ }
1324
+ });
1325
+ return;
1326
+ }
1327
+ case "libCall":
1328
+ if (!LIB_READS.test(e.fn))
1329
+ for (const a of e.args)
1330
+ escape(a, a.loc, `the library call ${e.fn}`);
1331
+ return;
1332
+ case "callValue":
1333
+ for (const a of e.args)
1334
+ escape(a, a.loc, "a call through a function value");
1335
+ if (e.receiver)
1336
+ escape(e.receiver, e.receiver.loc, "a call through a function value");
1337
+ return;
1338
+ }
1339
+ if (PASS_THROUGH.has(e.kind) || OBSERVE.has(e.kind))
1340
+ return;
1341
+ if (e.kind === "fieldGet" || e.kind === "recordGet" || e.kind === "arrayGet")
1342
+ return;
1343
+ // Any other parent: a derived child goes somewhere this pass does
1344
+ // not follow (dynamic conversions, islands, runtime helpers).
1345
+ everyExprChild(e, (child) => {
1346
+ escape(child, child.loc, describeSink(e));
1347
+ return true;
1348
+ }, () => true);
1349
+ },
1350
+ });
1351
+ }
1352
+ }
1353
+ /** A copy of a container whose elements may be immutable: the copy's
1354
+ * element slots may hold them too (the copy itself is fresh). */
1355
+ copyElements(receiver, result, receiverDerived, where) {
1356
+ const at = `copied at ${where}`;
1357
+ const rk = typeKey(receiver.type);
1358
+ const sources = [`e:${rk}`, `mk:${rk}`, `mv:${rk}`, `se:${rk}`];
1359
+ if (!receiverDerived && !sources.some((slot) => this.mayHold.has(slot)))
1360
+ return;
1361
+ const key = typeKey(result);
1362
+ for (const slot of [`e:${key}`, `se:${key}`, `mk:${key}`, `mv:${key}`])
1363
+ if (!this.mayHold.has(slot)) {
1364
+ this.mayHold.add(slot);
1365
+ this.via.set(slot, at);
1366
+ }
1367
+ }
1368
+ originOf(fn, e) {
1369
+ let root = e;
1370
+ for (let steps = 0; steps < 16; steps++) {
1371
+ if (root.kind === "fieldGet" || root.kind === "recordGet")
1372
+ root = root.obj;
1373
+ else if (root.kind === "arrayGet")
1374
+ root = root.arr;
1375
+ else if (root.kind === "upcast" ||
1376
+ root.kind === "downcast" ||
1377
+ root.kind === "unionWrap" ||
1378
+ root.kind === "unionNarrow" ||
1379
+ root.kind === "assignExpr")
1380
+ root = root.value;
1381
+ else if (root.kind === "varRef") {
1382
+ // Compiler temporaries (no source binding): follow their initializer.
1383
+ const id = root.localId;
1384
+ const local = fn.locals.find((l) => l.id === id);
1385
+ if (local === undefined || local.source !== undefined)
1386
+ break;
1387
+ let init = null;
1388
+ walk(fn.body, {
1389
+ stmt: (s) => {
1390
+ if (s.kind === "varDecl" && s.localId === id && s.init)
1391
+ init ??= s.init;
1392
+ },
1393
+ });
1394
+ if (init === null)
1395
+ break;
1396
+ root = init;
1397
+ }
1398
+ else
1399
+ break;
1400
+ }
1401
+ if (root.kind === "varRef") {
1402
+ const slot = this.localSlot(fn, root.localId);
1403
+ const note = this.annotated.get(slot);
1404
+ if (note)
1405
+ return ` (from '${note.name}')`;
1406
+ const via = this.via.get(slot);
1407
+ if (via) {
1408
+ const local = fn.locals.find((l) => l.id === root.localId);
1409
+ return ` ('${local?.name ?? root.localId}' may be an Immutable value ${via})`;
1410
+ }
1411
+ }
1412
+ return "";
1413
+ }
1414
+ /** Whether `e` may evaluate to (part of) an Immutable value. */
1415
+ derivedExpr(fn, e, derived) {
1416
+ switch (e.kind) {
1417
+ case "varRef":
1418
+ return this.mayHold.has(this.localSlot(fn, e.localId));
1419
+ case "fieldGet": {
1420
+ const slot = this.fieldSlot(e.className, e.field);
1421
+ return (slot !== null && this.mayHold.has(slot)) || derived(e.obj);
1422
+ }
1423
+ case "recordGet":
1424
+ return (this.mayHold.has(`s:${e.obj.type.kind === "record" ? e.obj.type.shapeId : ""}\0${e.field}`) || derived(e.obj));
1425
+ case "arrayGet":
1426
+ return this.mayHold.has(`e:${typeKey(e.arr.type)}`) || derived(e.arr);
1427
+ case "mapIntrinsic":
1428
+ return (MAP_ELEMENT_READS.has(e.method) &&
1429
+ (this.mayHold.has(`mv:${typeKey(e.receiver.type)}`) ||
1430
+ this.mayHold.has(`mk:${typeKey(e.receiver.type)}`) ||
1431
+ derived(e.receiver)));
1432
+ case "setIntrinsic":
1433
+ return (SET_ELEMENT_READS.has(e.method) &&
1434
+ (this.mayHold.has(`se:${typeKey(e.receiver.type)}`) || derived(e.receiver)));
1435
+ case "call":
1436
+ return this.mayHold.has(`r:${e.callee}`);
1437
+ case "virtualCall":
1438
+ return (this.methodImpls.get(e.method) ?? []).some((impl) => this.mayHold.has(`r:${impl}`));
1439
+ case "ternary":
1440
+ return derived(e.then) || derived(e.else_);
1441
+ case "logical":
1442
+ case "nullish":
1443
+ return derived(e.left) || derived(e.right);
1444
+ case "seqExpr":
1445
+ return derived(e.result);
1446
+ case "assignExpr":
1447
+ return derived(e.value);
1448
+ case "upcast":
1449
+ case "downcast":
1450
+ case "unionWrap":
1451
+ case "unionNarrow":
1452
+ return derived(e.value);
1453
+ default: {
1454
+ if (!PASS_THROUGH.has(e.kind))
1455
+ return false;
1456
+ let any = false;
1457
+ everyExprChild(e, (child) => {
1458
+ any ||= derived(child);
1459
+ return !any;
1460
+ }, () => true);
1461
+ return any;
1462
+ }
1463
+ }
1464
+ }
1465
+ /** Module constants with an Immutable annotation, written once from a
1466
+ * fresh value of a publishable type: the backend makes them immortal. */
1467
+ immortalGlobals() {
1468
+ const c = this.c;
1469
+ if (c.mod.lib !== undefined || c.diagnostics.length > 0)
1470
+ return;
1471
+ const out = [];
1472
+ for (const g of c.mod.globals ?? []) {
1473
+ if (g.mutable ||
1474
+ g.tdz === true ||
1475
+ g.initFlag !== undefined ||
1476
+ !this.annotated.has(`g:${g.id}`))
1477
+ continue;
1478
+ if (!this.publishable(g.type, new Set()))
1479
+ continue;
1480
+ let writes = 0;
1481
+ let fresh = true;
1482
+ for (const fn of c.mod.functions) {
1483
+ const freshLocals = this.freshLocalsOf(fn);
1484
+ walk(fn.body, {
1485
+ stmt: (s) => {
1486
+ if ((s.kind === "assign" || s.kind === "varDecl") && s.localId === g.id) {
1487
+ writes++;
1488
+ const value = s.kind === "assign" ? s.value : s.init;
1489
+ if (!value || !this.fresh(value, freshLocals))
1490
+ fresh = false;
1491
+ }
1492
+ },
1493
+ expr: (e) => {
1494
+ if (e.kind === "assignExpr" && e.localId === g.id)
1495
+ writes++;
1496
+ },
1497
+ });
1498
+ }
1499
+ if (writes === 1 && fresh)
1500
+ out.push(g.id);
1501
+ }
1502
+ c.record("immortalGlobals", out);
1503
+ }
1504
+ /** Static types the publication walkers handle without a refusal. */
1505
+ publishable(t, seen) {
1506
+ const c = this.c;
1507
+ switch (t.kind) {
1508
+ case "f64":
1509
+ case "bool":
1510
+ case "string":
1511
+ case "undefinedT":
1512
+ case "nullT":
1513
+ return true;
1514
+ case "array":
1515
+ case "set":
1516
+ return this.publishable(t.elem, seen);
1517
+ case "map":
1518
+ return this.publishable(t.key, seen) && this.publishable(t.value, seen);
1519
+ case "record": {
1520
+ const key = `r:${t.shapeId}`;
1521
+ if (seen.has(key))
1522
+ return true;
1523
+ seen.add(key);
1524
+ const shape = c.mod.records?.find((r) => r.id === t.shapeId);
1525
+ return (!!shape && !shape.indexValue && shape.fields.every((f) => this.publishable(f.type, seen)));
1526
+ }
1527
+ case "union": {
1528
+ const key = `u:${t.unionId}`;
1529
+ if (seen.has(key))
1530
+ return true;
1531
+ seen.add(key);
1532
+ const def = c.mod.unions?.find((u) => u.id === t.unionId);
1533
+ return !!def && def.arms.every((arm) => this.publishable(arm, seen));
1534
+ }
1535
+ case "object": {
1536
+ const key = `o:${t.className}`;
1537
+ if (seen.has(key))
1538
+ return true;
1539
+ seen.add(key);
1540
+ // Subclasses can stand behind a base-typed reference.
1541
+ for (const cls of c.classes.values()) {
1542
+ let under = false;
1543
+ for (let x = cls; x; x = x.base ? c.classes.get(x.base) : undefined)
1544
+ if (x.name === t.className)
1545
+ under = true;
1546
+ if (!under)
1547
+ continue;
1548
+ for (let x = cls; x; x = x.base ? c.classes.get(x.base) : undefined)
1549
+ if (x.runtime === true || x.localCaptures !== undefined)
1550
+ return false;
1551
+ if (!cls.fields.every((f) => f.name.startsWith("%") || this.publishable(f.type, seen)))
1552
+ return false;
1553
+ }
1554
+ return true;
1555
+ }
1556
+ default:
1557
+ return false;
1558
+ }
1559
+ }
1560
+ }
1561
+ function describeSink(e) {
1562
+ switch (e.kind) {
1563
+ case "dynFrom":
1564
+ case "dynObjLit":
1565
+ case "dynArrLit":
1566
+ return "dynamic (any or unknown) code";
1567
+ case "jsOp":
1568
+ case "jsMarshal":
1569
+ case "jsExit":
1570
+ return "the JavaScript island";
1571
+ case "intrinsic":
1572
+ return `the runtime (${e.name})`;
1573
+ default:
1574
+ return `an operation scriptc does not follow (${e.kind})`;
1575
+ }
1576
+ }
1577
+ //# sourceMappingURL=check.js.map