@scriptc/compiler 0.3.4 → 0.3.5
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.
- package/ambient/scriptc-node-fallback.d.ts +131 -10
- package/dist/backend/llvm/array-search.js +1 -1
- package/dist/backend/llvm/array-search.js.map +1 -1
- package/dist/backend/llvm/call-lifetimes.d.ts +6 -0
- package/dist/backend/llvm/call-lifetimes.js +19 -0
- package/dist/backend/llvm/call-lifetimes.js.map +1 -1
- package/dist/backend/llvm/constant-callbacks.d.ts +2 -21
- package/dist/backend/llvm/constant-callbacks.js +36 -219
- package/dist/backend/llvm/constant-callbacks.js.map +1 -1
- package/dist/backend/llvm/constant-callbacks.test.js +22 -0
- package/dist/backend/llvm/constant-callbacks.test.js.map +1 -1
- package/dist/backend/llvm/dense-array-access.d.ts +9 -0
- package/dist/backend/llvm/dense-array-access.js +27 -1
- package/dist/backend/llvm/dense-array-access.js.map +1 -1
- package/dist/backend/llvm/dyn.d.ts +6 -0
- package/dist/backend/llvm/dyn.js +40 -6
- package/dist/backend/llvm/dyn.js.map +1 -1
- package/dist/backend/llvm/emitter.d.ts +75 -1
- package/dist/backend/llvm/emitter.js +408 -38
- package/dist/backend/llvm/emitter.js.map +1 -1
- package/dist/backend/llvm/expr-async.js +17 -7
- package/dist/backend/llvm/expr-async.js.map +1 -1
- package/dist/backend/llvm/expr-bytes.js +1 -1
- package/dist/backend/llvm/expr-callbacks.d.ts +11 -0
- package/dist/backend/llvm/expr-callbacks.js +53 -0
- package/dist/backend/llvm/expr-callbacks.js.map +1 -1
- package/dist/backend/llvm/expr-calls.js +88 -2
- package/dist/backend/llvm/expr-calls.js.map +1 -1
- package/dist/backend/llvm/expr-containers.js +50 -27
- package/dist/backend/llvm/expr-containers.js.map +1 -1
- package/dist/backend/llvm/expr-primitives.js +9 -1
- package/dist/backend/llvm/expr-primitives.js.map +1 -1
- package/dist/backend/llvm/expr-stream-bridges.js +4 -0
- package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
- package/dist/backend/llvm/integer-clones.d.ts +103 -0
- package/dist/backend/llvm/integer-clones.js +1336 -0
- package/dist/backend/llvm/integer-clones.js.map +1 -0
- package/dist/backend/llvm/integer-clones.test.d.ts +1 -0
- package/dist/backend/llvm/integer-clones.test.js +219 -0
- package/dist/backend/llvm/integer-clones.test.js.map +1 -0
- package/dist/backend/llvm/lib-datagram.js +14 -0
- package/dist/backend/llvm/lib-datagram.js.map +1 -1
- package/dist/backend/llvm/lib-dispatch.js +4 -0
- package/dist/backend/llvm/lib-dispatch.js.map +1 -1
- package/dist/backend/llvm/lib-process.js +24 -0
- package/dist/backend/llvm/lib-process.js.map +1 -1
- package/dist/backend/llvm/lib-shared.js +34 -2
- package/dist/backend/llvm/lib-shared.js.map +1 -1
- package/dist/backend/llvm/lib-tls.js +6 -0
- package/dist/backend/llvm/lib-tls.js.map +1 -1
- package/dist/backend/llvm/local-array-reads.js +1 -1
- package/dist/backend/llvm/local-array-reads.js.map +1 -1
- package/dist/backend/llvm/map-entry-fusion.d.ts +28 -0
- package/dist/backend/llvm/map-entry-fusion.js +233 -0
- package/dist/backend/llvm/map-entry-fusion.js.map +1 -0
- package/dist/backend/llvm/map-entry-fusion.test.d.ts +1 -0
- package/dist/backend/llvm/map-entry-fusion.test.js +183 -0
- package/dist/backend/llvm/map-entry-fusion.test.js.map +1 -0
- package/dist/backend/llvm/map-number-lookup.d.ts +2 -0
- package/dist/backend/llvm/map-number-lookup.js +1 -1
- package/dist/backend/llvm/map-number-lookup.js.map +1 -1
- package/dist/backend/llvm/map-number-lookup.test.js +10 -11
- package/dist/backend/llvm/map-number-lookup.test.js.map +1 -1
- package/dist/backend/llvm/map-read-lifetimes.d.ts +2 -0
- package/dist/backend/llvm/map-read-lifetimes.js +3 -0
- package/dist/backend/llvm/map-read-lifetimes.js.map +1 -1
- package/dist/backend/llvm/map-read-lifetimes.test.js +2 -2
- package/dist/backend/llvm/publish.d.ts +16 -0
- package/dist/backend/llvm/publish.js +81 -10
- package/dist/backend/llvm/publish.js.map +1 -1
- package/dist/backend/llvm/publish.test.d.ts +1 -0
- package/dist/backend/llvm/publish.test.js +94 -0
- package/dist/backend/llvm/publish.test.js.map +1 -0
- package/dist/backend/llvm/record-results.js +76 -2
- package/dist/backend/llvm/record-results.js.map +1 -1
- package/dist/backend/llvm/record-sources.d.ts +7 -0
- package/dist/backend/llvm/record-sources.js +14 -0
- package/dist/backend/llvm/record-sources.js.map +1 -0
- package/dist/backend/llvm/shapes.d.ts +9 -3
- package/dist/backend/llvm/shapes.js +18 -14
- package/dist/backend/llvm/shapes.js.map +1 -1
- package/dist/backend/llvm/shapes.test.js +5 -3
- package/dist/backend/llvm/shapes.test.js.map +1 -1
- package/dist/backend/llvm/string-lifetimes.test.js +75 -1
- package/dist/backend/llvm/string-lifetimes.test.js.map +1 -1
- package/dist/backend/llvm/walkers.d.ts +17 -0
- package/dist/backend/llvm/walkers.js +123 -0
- package/dist/backend/llvm/walkers.js.map +1 -1
- package/dist/backend/native/contracts.d.ts +2 -1
- package/dist/backend/native/executable.js +3 -1
- package/dist/backend/native/executable.js.map +1 -1
- package/dist/backend/runtime-abi.d.ts +2 -2
- package/dist/backend/runtime-abi.js +2 -2
- package/dist/backend/runtime-pack-targets.test.js +1 -1
- package/dist/backend/runtime-pack.test.js +1 -1
- package/dist/frontend/lowering/builtin-calls.js +3 -0
- package/dist/frontend/lowering/builtin-calls.js.map +1 -1
- package/dist/frontend/lowering/builtins/callable-aliases.js +54 -8
- package/dist/frontend/lowering/builtins/callable-aliases.js.map +1 -1
- package/dist/frontend/lowering/builtins/child-process-async.d.ts +8 -6
- package/dist/frontend/lowering/builtins/child-process-async.js +45 -29
- package/dist/frontend/lowering/builtins/child-process-async.js.map +1 -1
- package/dist/frontend/lowering/builtins/filesystem-calls.d.ts +2 -0
- package/dist/frontend/lowering/builtins/filesystem-calls.js +5 -0
- package/dist/frontend/lowering/builtins/filesystem-calls.js.map +1 -1
- package/dist/frontend/lowering/builtins/filesystem.js +60 -0
- package/dist/frontend/lowering/builtins/filesystem.js.map +1 -1
- package/dist/frontend/lowering/builtins/fs-streams.d.ts +26 -0
- package/dist/frontend/lowering/builtins/fs-streams.js +131 -0
- package/dist/frontend/lowering/builtins/fs-streams.js.map +1 -0
- package/dist/frontend/lowering/builtins/module-calls.js +5 -1
- package/dist/frontend/lowering/builtins/module-calls.js.map +1 -1
- package/dist/frontend/lowering/class-dynamic-dispatch.js +150 -8
- package/dist/frontend/lowering/class-dynamic-dispatch.js.map +1 -1
- package/dist/frontend/lowering/containers/bytes.js +33 -12
- package/dist/frontend/lowering/containers/bytes.js.map +1 -1
- package/dist/frontend/lowering/devirtualize.js +20 -7
- package/dist/frontend/lowering/devirtualize.js.map +1 -1
- package/dist/frontend/lowering/lower-calls.js +58 -3
- package/dist/frontend/lowering/lower-calls.js.map +1 -1
- package/dist/frontend/lowering/lower-classes.js +7 -0
- package/dist/frontend/lowering/lower-classes.js.map +1 -1
- package/dist/frontend/lowering/lower-dgram.js +55 -15
- package/dist/frontend/lowering/lower-dgram.js.map +1 -1
- package/dist/frontend/lowering/lower-event-emitter.d.ts +9 -1
- package/dist/frontend/lowering/lower-event-emitter.js +78 -0
- package/dist/frontend/lowering/lower-event-emitter.js.map +1 -1
- package/dist/frontend/lowering/lower-exprs.d.ts +4 -0
- package/dist/frontend/lowering/lower-exprs.js +132 -58
- package/dist/frontend/lowering/lower-exprs.js.map +1 -1
- package/dist/frontend/lowering/lower-inspect.js +26 -0
- package/dist/frontend/lowering/lower-inspect.js.map +1 -1
- package/dist/frontend/lowering/lower-server.d.ts +2 -1
- package/dist/frontend/lowering/lower-server.js +73 -5
- package/dist/frontend/lowering/lower-server.js.map +1 -1
- package/dist/frontend/lowering/lower-stmts.js +12 -6
- package/dist/frontend/lowering/lower-stmts.js.map +1 -1
- package/dist/frontend/lowering/lower-stream.js +8 -0
- package/dist/frontend/lowering/lower-stream.js.map +1 -1
- package/dist/frontend/lowering/lowerer.d.ts +3 -1
- package/dist/frontend/lowering/lowerer.js +2 -2
- package/dist/frontend/lowering/lowerer.js.map +1 -1
- package/dist/frontend/lowering/record-sources.d.ts +15 -0
- package/dist/frontend/lowering/record-sources.js +46 -0
- package/dist/frontend/lowering/record-sources.js.map +1 -0
- package/dist/frontend/lowering/surfaces.js +33 -1
- package/dist/frontend/lowering/surfaces.js.map +1 -1
- package/dist/frontend/type-mapper.d.ts +3 -0
- package/dist/frontend/type-mapper.js +53 -0
- package/dist/frontend/type-mapper.js.map +1 -1
- package/dist/ir/analysis.d.ts +10 -0
- package/dist/ir/analysis.js +35 -0
- package/dist/ir/analysis.js.map +1 -1
- package/dist/ir/analysis.test.js +69 -1
- package/dist/ir/analysis.test.js.map +1 -1
- package/dist/ir/builtin-effects.js +36 -0
- package/dist/ir/builtin-effects.js.map +1 -1
- package/dist/ir/builtin-signatures.d.ts +1 -1
- package/dist/ir/builtin-signatures.js +91 -7
- package/dist/ir/builtin-signatures.js.map +1 -1
- package/dist/ir/capture-values.d.ts +107 -0
- package/dist/ir/capture-values.js +850 -0
- package/dist/ir/capture-values.js.map +1 -0
- package/dist/ir/capture-values.test.d.ts +1 -0
- package/dist/ir/capture-values.test.js +268 -0
- package/dist/ir/capture-values.test.js.map +1 -0
- package/dist/ir/field-values.d.ts +16 -0
- package/dist/ir/field-values.js +128 -0
- package/dist/ir/field-values.js.map +1 -0
- package/dist/ir/field-values.test.d.ts +1 -0
- package/dist/ir/field-values.test.js +160 -0
- package/dist/ir/field-values.test.js.map +1 -0
- package/dist/ir/final-fields.d.ts +4 -1
- package/dist/ir/final-fields.js +11 -3
- package/dist/ir/final-fields.js.map +1 -1
- package/dist/ir/function-references.test.js +1 -1
- package/dist/ir/function-references.test.js.map +1 -1
- package/dist/ir/init-only-globals.d.ts +16 -0
- package/dist/ir/init-only-globals.js +121 -0
- package/dist/ir/init-only-globals.js.map +1 -0
- package/dist/ir/init-only-globals.test.d.ts +1 -0
- package/dist/ir/init-only-globals.test.js +115 -0
- package/dist/ir/init-only-globals.test.js.map +1 -0
- package/dist/ir/initializer-bindings.d.ts +18 -1
- package/dist/ir/initializer-bindings.js +91 -1
- package/dist/ir/initializer-bindings.js.map +1 -1
- package/dist/ir/initializer-bindings.test.js +57 -2
- package/dist/ir/initializer-bindings.test.js.map +1 -1
- package/dist/ir/int32-slots.d.ts +22 -0
- package/dist/ir/int32-slots.js +376 -13
- package/dist/ir/int32-slots.js.map +1 -1
- package/dist/ir/int32-slots.test.js +91 -8
- package/dist/ir/int32-slots.test.js.map +1 -1
- package/dist/ir/integer-loop-facts.d.ts +19 -1
- package/dist/ir/integer-loop-facts.js +155 -7
- package/dist/ir/integer-loop-facts.js.map +1 -1
- package/dist/ir/integer-ranges.d.ts +8 -5
- package/dist/ir/integer-ranges.js +147 -55
- package/dist/ir/integer-ranges.js.map +1 -1
- package/dist/ir/integer-ranges.test.js +225 -7
- package/dist/ir/integer-ranges.test.js.map +1 -1
- package/dist/ir/ir.d.ts +9 -1
- package/dist/ir/ir.js.map +1 -1
- package/dist/ir/property-bags.d.ts +14 -0
- package/dist/ir/property-bags.js +121 -0
- package/dist/ir/property-bags.js.map +1 -0
- package/dist/ir/receiver-readers.d.ts +21 -0
- package/dist/ir/receiver-readers.js +217 -0
- package/dist/ir/receiver-readers.js.map +1 -0
- package/dist/ir/runtime-features.js +6 -1
- package/dist/ir/runtime-features.js.map +1 -1
- package/dist/ir/validate.js +26 -0
- package/dist/ir/validate.js.map +1 -1
- package/dist/ir/validate.test.js +21 -0
- package/dist/ir/validate.test.js.map +1 -1
- package/dist/library/int-infer.js +3 -0
- package/dist/library/int-infer.js.map +1 -1
- package/package.json +20 -20
- package/surface-manifest.json +28 -2
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import { typeEquals, } from "./ir.js";
|
|
2
|
+
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
|
|
3
|
+
import { AmbientReceiverReaders } from "./receiver-readers.js";
|
|
4
|
+
import { reparseStmt } from "./serialize.js";
|
|
5
|
+
import { everyExprChild, everyStmtChild, everyStmtList, mapExprChildren, mapStmtChildren, } from "./traverse.js";
|
|
6
|
+
/** Captured bindings that hold one value for their whole observable life
|
|
7
|
+
* read through plain immutable locals.
|
|
8
|
+
*
|
|
9
|
+
* A captured binding lives in a shared box: the declaring function and
|
|
10
|
+
* every closure environment that captures it address the same cell, so a
|
|
11
|
+
* write anywhere is visible everywhere. When nothing ever writes the cell
|
|
12
|
+
* after its declaration fills it, the box is only a carrier: every read
|
|
13
|
+
* observes the declaration's value. Analyses and code generation exclude
|
|
14
|
+
* boxed locals, though, so a boxed `const N` turns all integer index math
|
|
15
|
+
* that reads it into checked f64 arithmetic and every reference read into
|
|
16
|
+
* an out-of-line retained box load.
|
|
17
|
+
*
|
|
18
|
+
* This pass keeps the boxes (environments still capture and trace them
|
|
19
|
+
* exactly as before, so ownership and cycle collection are unchanged) and
|
|
20
|
+
* moves the reads onto plain locals:
|
|
21
|
+
*
|
|
22
|
+
* - In the declaring function, `varDecl x = init` becomes
|
|
23
|
+
* `varDecl x' = init; varDecl x = x'`, and every read of `x` reads `x'`.
|
|
24
|
+
* An unchanged captured number parameter `p` gets `varDecl p' = p` at
|
|
25
|
+
* entry in the same way.
|
|
26
|
+
* - In a closure body, an entry `varDecl c' = c` copies the environment's
|
|
27
|
+
* value once and every read of `c` reads `c'`. When every creation site
|
|
28
|
+
* captures a declaration of the same closed numeric constant, `c'`
|
|
29
|
+
* starts from that literal instead, so range analysis sees the constant.
|
|
30
|
+
*
|
|
31
|
+
* Nested environments still capture the original box. A call through such
|
|
32
|
+
* a binding whose only value is an environment-free function (a nested
|
|
33
|
+
* function declaration or arrow that captures nothing) becomes a direct
|
|
34
|
+
* call of a capture-free copy of that body, as a call of a top-level
|
|
35
|
+
* function is: the closure value itself still exists for every other use.
|
|
36
|
+
*
|
|
37
|
+
* Soundness. A binding qualifies when its box is filled exactly once
|
|
38
|
+
* before any environment can observe it and nothing rebinds it in any
|
|
39
|
+
* function that shares the box:
|
|
40
|
+
*
|
|
41
|
+
* - the declaration is the binding's only `varDecl` and has an
|
|
42
|
+
* initializer that does not itself create an environment over it; a
|
|
43
|
+
* temporal-dead-zone binding (an environment exists before its
|
|
44
|
+
* initializing `assign`) qualifies only when no code can run any such
|
|
45
|
+
* environment, or read the binding, before that assignment (orderedTdz),
|
|
46
|
+
* and otherwise keeps every checked read,
|
|
47
|
+
* - no `assign`, `assignExpr`, `incDec`, `forOf`, catch or rethrow binding
|
|
48
|
+
* targets it, here or in any closure body reached through capture edges,
|
|
49
|
+
* - local classes keep their boxes (method bodies are not followed).
|
|
50
|
+
*
|
|
51
|
+
* A closure capture qualifies when every creation site supplies such a
|
|
52
|
+
* binding, an unchanged parameter, or a qualifying capture of the creating
|
|
53
|
+
* closure. A closure can only run after its creation, so its entry copy
|
|
54
|
+
* reads the final value. Every execution of a declaration (for example
|
|
55
|
+
* per loop iteration) fills its own fresh box, so environments created in
|
|
56
|
+
* different iterations keep seeing their own value. */
|
|
57
|
+
export function copyStableCaptures(mod) {
|
|
58
|
+
const flow = new CaptureFlow(mod.functions);
|
|
59
|
+
let changed = false;
|
|
60
|
+
const functions = mod.functions.map((fn) => {
|
|
61
|
+
const next = flow.rewrite(fn);
|
|
62
|
+
if (next !== fn)
|
|
63
|
+
changed = true;
|
|
64
|
+
return next;
|
|
65
|
+
});
|
|
66
|
+
if (!changed)
|
|
67
|
+
return mod;
|
|
68
|
+
const rewritten = new Map(functions.map((fn) => [fn.name, fn]));
|
|
69
|
+
for (const [name, clone] of flow.directClones()) {
|
|
70
|
+
const { captures: _captures, ...plain } = rewritten.get(name);
|
|
71
|
+
// Analyses key facts by node identity: the copy gets its own nodes.
|
|
72
|
+
functions.push({ ...plain, name: clone, body: plain.body.map(reparseStmt) });
|
|
73
|
+
}
|
|
74
|
+
// A closure body whose every use became a direct call is only reachable
|
|
75
|
+
// through its copy now.
|
|
76
|
+
const referenced = new Set(moduleFunctionReferences(mod));
|
|
77
|
+
for (const fn of functions)
|
|
78
|
+
everyStmtList(fn.body, {
|
|
79
|
+
stmt: () => true,
|
|
80
|
+
expr: (e) => {
|
|
81
|
+
const name = e.kind === "call" ? e.callee : exprFunctionReference(e);
|
|
82
|
+
if (name !== undefined)
|
|
83
|
+
referenced.add(name);
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const clones = flow.directClones();
|
|
88
|
+
return {
|
|
89
|
+
...mod,
|
|
90
|
+
functions: functions
|
|
91
|
+
.filter((fn) => !clones.has(fn.name) || referenced.has(fn.name))
|
|
92
|
+
.map((fn) => {
|
|
93
|
+
const clone = clones.get(fn.name);
|
|
94
|
+
return clone === undefined ? fn : forwardTo(fn, clone);
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** A closure body that other uses still reach as a value, reduced to a call
|
|
99
|
+
* of its capture-free copy so the body exists once in the module. The copy
|
|
100
|
+
* takes the same parameters, and the body neither names its own closure nor
|
|
101
|
+
* reads the ambient receiver (CaptureFlow.direct), so the call is the whole
|
|
102
|
+
* behavior. */
|
|
103
|
+
function forwardTo(fn, clone) {
|
|
104
|
+
const call = {
|
|
105
|
+
kind: "call",
|
|
106
|
+
callee: clone,
|
|
107
|
+
args: fn.params.map((p) => ({ kind: "varRef", localId: p.localId, type: p.type, loc: fn.loc })),
|
|
108
|
+
type: fn.returnType,
|
|
109
|
+
loc: fn.loc,
|
|
110
|
+
};
|
|
111
|
+
const params = new Set(fn.params.map((p) => p.localId));
|
|
112
|
+
return {
|
|
113
|
+
...fn,
|
|
114
|
+
// Parameters only flow into the call now: nothing captures them.
|
|
115
|
+
locals: fn.locals.filter((l) => params.has(l.id)).map(plainLocal),
|
|
116
|
+
body: fn.returnType.kind === "void"
|
|
117
|
+
? [{ kind: "exprStmt", expr: call, loc: fn.loc }]
|
|
118
|
+
: [{ kind: "return", value: call, loc: fn.loc }],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** A parameter local without the capture box or TDZ flag of its old body. */
|
|
122
|
+
function plainLocal(local) {
|
|
123
|
+
const plain = {
|
|
124
|
+
id: local.id,
|
|
125
|
+
name: local.name,
|
|
126
|
+
type: local.type,
|
|
127
|
+
mutable: local.mutable,
|
|
128
|
+
};
|
|
129
|
+
if (local.source !== undefined)
|
|
130
|
+
plain.source = local.source;
|
|
131
|
+
return plain;
|
|
132
|
+
}
|
|
133
|
+
/** The value of a closed numeric expression over literals (`1024`,
|
|
134
|
+
* `-1`, `4 * 256`), evaluated with the same IEEE double arithmetic. */
|
|
135
|
+
function constantNumber(e) {
|
|
136
|
+
if (e.type.kind !== "f64")
|
|
137
|
+
return null;
|
|
138
|
+
if (e.kind === "numLit")
|
|
139
|
+
return e.value;
|
|
140
|
+
if (e.kind === "unary" && e.op === "-") {
|
|
141
|
+
const operand = constantNumber(e.operand);
|
|
142
|
+
return operand === null ? null : -operand;
|
|
143
|
+
}
|
|
144
|
+
if (e.kind !== "bin")
|
|
145
|
+
return null;
|
|
146
|
+
const left = constantNumber(e.left);
|
|
147
|
+
const right = left === null ? null : constantNumber(e.right);
|
|
148
|
+
if (left === null || right === null)
|
|
149
|
+
return null;
|
|
150
|
+
switch (e.op) {
|
|
151
|
+
case "+":
|
|
152
|
+
return left + right;
|
|
153
|
+
case "-":
|
|
154
|
+
return left - right;
|
|
155
|
+
case "*":
|
|
156
|
+
return left * right;
|
|
157
|
+
case "/":
|
|
158
|
+
return left / right;
|
|
159
|
+
default:
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Literals worth propagating: special values keep their declaration. */
|
|
164
|
+
function plainLiteral(value) {
|
|
165
|
+
return value !== null && Number.isFinite(value) && !Object.is(value, -0) ? value : null;
|
|
166
|
+
}
|
|
167
|
+
/** `body` without the definitions of the `dead` bindings: their empty
|
|
168
|
+
* declarations and the declarations or assignments binding a closure. */
|
|
169
|
+
function prune(body, dead) {
|
|
170
|
+
const list = (stmts) => stmts.flatMap((s) => {
|
|
171
|
+
switch (s.kind) {
|
|
172
|
+
case "varDecl":
|
|
173
|
+
return dead.has(s.localId) && (s.init === null || s.init.kind === "closure") ? [] : [s];
|
|
174
|
+
case "assign":
|
|
175
|
+
return dead.has(s.localId) && s.value.kind === "closure" ? [] : [s];
|
|
176
|
+
case "block":
|
|
177
|
+
return [{ ...s, body: list(s.body) }];
|
|
178
|
+
case "if":
|
|
179
|
+
return [{ ...s, then: list(s.then), else_: s.else_ === null ? null : list(s.else_) }];
|
|
180
|
+
case "while":
|
|
181
|
+
case "doWhile":
|
|
182
|
+
case "for":
|
|
183
|
+
case "forOf":
|
|
184
|
+
return [{ ...s, body: list(s.body) }];
|
|
185
|
+
case "tryCatch":
|
|
186
|
+
return [
|
|
187
|
+
{
|
|
188
|
+
...s,
|
|
189
|
+
tryBody: list(s.tryBody),
|
|
190
|
+
catchBody: s.catchBody === null ? null : list(s.catchBody),
|
|
191
|
+
finallyBody: s.finallyBody === null ? null : list(s.finallyBody),
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
default:
|
|
195
|
+
return [s];
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return list(body);
|
|
199
|
+
}
|
|
200
|
+
/** Capture-flow facts over the whole module. Capture chains follow lexical
|
|
201
|
+
* nesting and cannot cycle; provisional memo entries only bound work on
|
|
202
|
+
* malformed input. */
|
|
203
|
+
export class CaptureFlow {
|
|
204
|
+
functions = new Map();
|
|
205
|
+
facts = new Map();
|
|
206
|
+
unchangedMemo = new Map();
|
|
207
|
+
stableMemo = new Map();
|
|
208
|
+
sites = new Map();
|
|
209
|
+
receivers;
|
|
210
|
+
directMemo = new Map();
|
|
211
|
+
/** Environment-free bodies called directly, and their copies' names. */
|
|
212
|
+
clones = new Map();
|
|
213
|
+
constructor(functions) {
|
|
214
|
+
for (const fn of functions)
|
|
215
|
+
this.functions.set(fn.name, fn);
|
|
216
|
+
this.receivers = new AmbientReceiverReaders(this.functions);
|
|
217
|
+
// Creation sites are collected while scanning each body.
|
|
218
|
+
for (const fn of functions)
|
|
219
|
+
this.factsOf(fn);
|
|
220
|
+
}
|
|
221
|
+
directClones() {
|
|
222
|
+
return this.clones;
|
|
223
|
+
}
|
|
224
|
+
/** A lifted body that captures nothing and can be entered like a declared
|
|
225
|
+
* function: synchronous, never naming its own closure, and unable to
|
|
226
|
+
* observe the ambient receiver a closure call would bracket. */
|
|
227
|
+
direct(fnName) {
|
|
228
|
+
const known = this.directMemo.get(fnName);
|
|
229
|
+
if (known !== undefined)
|
|
230
|
+
return known;
|
|
231
|
+
const fn = this.functions.get(fnName);
|
|
232
|
+
const result = fn !== undefined &&
|
|
233
|
+
fn.captures?.length === 0 &&
|
|
234
|
+
fn.classCaptures === undefined &&
|
|
235
|
+
fn.async !== true &&
|
|
236
|
+
fn.generator === undefined &&
|
|
237
|
+
everyStmtList(fn.body, { stmt: () => true, expr: (e) => e.kind !== "selfRef" }) &&
|
|
238
|
+
this.receivers.ignores(fn);
|
|
239
|
+
this.directMemo.set(fnName, result);
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
/** The environment-free function every execution binds to `id`, if any. */
|
|
243
|
+
target(fnName, id, depth = 0) {
|
|
244
|
+
const fn = this.functions.get(fnName);
|
|
245
|
+
if (fn === undefined || depth > 64)
|
|
246
|
+
return null;
|
|
247
|
+
const facts = this.factsOf(fn);
|
|
248
|
+
const own = facts.declarations.get(id);
|
|
249
|
+
if (own !== undefined) {
|
|
250
|
+
const local = facts.locals.get(id);
|
|
251
|
+
if (local?.boxed === true ? !this.stableOrigin(fn, id) : !this.plainConstant(fn, id))
|
|
252
|
+
return null;
|
|
253
|
+
const hoisted = facts.hoisted.get(id);
|
|
254
|
+
const value = own.initialized ? own.init : hoisted?.kind === "assign" ? hoisted.value : null;
|
|
255
|
+
return value?.kind === "closure" &&
|
|
256
|
+
value.captures.length === 0 &&
|
|
257
|
+
value.adapts !== true &&
|
|
258
|
+
this.direct(value.fnName)
|
|
259
|
+
? value.fnName
|
|
260
|
+
: null;
|
|
261
|
+
}
|
|
262
|
+
const index = fn.captures?.findIndex((c) => c.localId === id) ?? -1;
|
|
263
|
+
if (index < 0 || !this.stableCapture(fnName, id))
|
|
264
|
+
return null;
|
|
265
|
+
let result = null;
|
|
266
|
+
for (const site of this.sites.get(fnName) ?? []) {
|
|
267
|
+
const source = site.captures[index];
|
|
268
|
+
const next = source === undefined ? null : this.target(site.owner, source, depth + 1);
|
|
269
|
+
if (next === null || (result !== null && result !== next))
|
|
270
|
+
return null;
|
|
271
|
+
result = next;
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
statementIds = new Map();
|
|
276
|
+
/** The bindings a statement reads (with counts) and captures, at any depth. */
|
|
277
|
+
idsIn(s) {
|
|
278
|
+
const known = this.statementIds.get(s);
|
|
279
|
+
if (known !== undefined)
|
|
280
|
+
return known;
|
|
281
|
+
const result = { reads: new Map(), captures: new Set() };
|
|
282
|
+
const expr = (e) => {
|
|
283
|
+
if (e.kind === "varRef")
|
|
284
|
+
result.reads.set(e.localId, (result.reads.get(e.localId) ?? 0) + 1);
|
|
285
|
+
else if (e.kind === "closure" || e.kind === "classRef")
|
|
286
|
+
for (const id of e.captures ?? [])
|
|
287
|
+
result.captures.add(id);
|
|
288
|
+
return everyExprChild(e, expr, stmt);
|
|
289
|
+
};
|
|
290
|
+
const stmt = (node) => everyStmtChild(node, expr, stmt);
|
|
291
|
+
stmt(s);
|
|
292
|
+
this.statementIds.set(s, result);
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
/** Reads of `id` in the statements of `body` after index `after`. */
|
|
296
|
+
readsAfter(body, after, id) {
|
|
297
|
+
let count = 0;
|
|
298
|
+
for (let k = after + 1; k < body.length; k++)
|
|
299
|
+
count += this.idsIn(body[k]).reads.get(id) ?? 0;
|
|
300
|
+
return count;
|
|
301
|
+
}
|
|
302
|
+
/** A temporal-dead-zone binding no read can reach before its
|
|
303
|
+
* initialization: every environment created before the initializing
|
|
304
|
+
* assignment is the direct value of an unshared local of this function
|
|
305
|
+
* whose every read follows that assignment, and so is every read of the
|
|
306
|
+
* binding itself. Its box is then filled before any code can observe it. */
|
|
307
|
+
orderedTdz(fn, id) {
|
|
308
|
+
const facts = this.factsOf(fn);
|
|
309
|
+
const init = facts.tdzInits.get(id);
|
|
310
|
+
if (init === undefined || (facts.writes.get(id) ?? 0) !== 1)
|
|
311
|
+
return false;
|
|
312
|
+
const { body, declared, initialized } = init;
|
|
313
|
+
if ((facts.reads.get(id) ?? 0) !== this.readsAfter(body, initialized, id))
|
|
314
|
+
return false;
|
|
315
|
+
for (let k = declared + 1; k < body.length; k++) {
|
|
316
|
+
const s = body[k];
|
|
317
|
+
if (!this.idsIn(s).captures.has(id))
|
|
318
|
+
continue;
|
|
319
|
+
if (k > initialized)
|
|
320
|
+
continue;
|
|
321
|
+
if (k === initialized)
|
|
322
|
+
return false;
|
|
323
|
+
let holder;
|
|
324
|
+
let value;
|
|
325
|
+
if (s.kind === "assign") {
|
|
326
|
+
holder = s.localId;
|
|
327
|
+
value = s.value;
|
|
328
|
+
}
|
|
329
|
+
else if (s.kind === "varDecl") {
|
|
330
|
+
holder = s.localId;
|
|
331
|
+
value = s.init;
|
|
332
|
+
}
|
|
333
|
+
else
|
|
334
|
+
return false;
|
|
335
|
+
if (value?.kind !== "closure")
|
|
336
|
+
return false;
|
|
337
|
+
const local = facts.locals.get(holder);
|
|
338
|
+
if (local === undefined || local.boxed === true || local.tdz === true)
|
|
339
|
+
return false;
|
|
340
|
+
const declaration = facts.declarations.get(holder);
|
|
341
|
+
const writes = facts.writes.get(holder) ?? 0;
|
|
342
|
+
if (declaration?.count !== 1 || writes !== (s.kind === "assign" ? 1 : 0))
|
|
343
|
+
return false;
|
|
344
|
+
if ((facts.reads.get(holder) ?? 0) !== this.readsAfter(body, initialized, holder))
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
// Every environment over the binding is created in this list.
|
|
348
|
+
let nodes = 0;
|
|
349
|
+
const count = (e) => {
|
|
350
|
+
if (e.kind === "closure" && e.captures.includes(id))
|
|
351
|
+
nodes++;
|
|
352
|
+
return true;
|
|
353
|
+
};
|
|
354
|
+
everyStmtList(body.slice(declared + 1), { stmt: () => true, expr: count });
|
|
355
|
+
const edges = facts.edges.filter((edge) => edge.source === id).length;
|
|
356
|
+
return nodes === edges && !facts.classCaptured.has(id);
|
|
357
|
+
}
|
|
358
|
+
/** An unboxed binding (no environment shares it) of `fn` itself that
|
|
359
|
+
* holds its single definition from its first read on. */
|
|
360
|
+
plainConstant(fn, id) {
|
|
361
|
+
const facts = this.factsOf(fn);
|
|
362
|
+
const local = facts.locals.get(id);
|
|
363
|
+
const declaration = facts.declarations.get(id);
|
|
364
|
+
if (local === undefined || local.boxed === true || local.tdz === true)
|
|
365
|
+
return false;
|
|
366
|
+
if (fn.params.some((p) => p.localId === id) || declaration?.count !== 1)
|
|
367
|
+
return false;
|
|
368
|
+
const writes = facts.writes.get(id) ?? 0;
|
|
369
|
+
return declaration.initialized ? writes === 0 : facts.hoisted.has(id) && writes === 1;
|
|
370
|
+
}
|
|
371
|
+
/** `callValue` through a binding bound to one environment-free function,
|
|
372
|
+
* as a direct call of its capture-free copy. */
|
|
373
|
+
directCall(fnName, e) {
|
|
374
|
+
if (e.receiver !== undefined || e.callee.kind !== "varRef")
|
|
375
|
+
return null;
|
|
376
|
+
const target = this.target(fnName, e.callee.localId);
|
|
377
|
+
const callee = target === null ? undefined : this.functions.get(target);
|
|
378
|
+
const ft = e.callee.type;
|
|
379
|
+
if (callee === undefined || ft.kind !== "func")
|
|
380
|
+
return null;
|
|
381
|
+
if (callee.params.length !== e.args.length ||
|
|
382
|
+
ft.params.length !== e.args.length ||
|
|
383
|
+
!typeEquals(callee.returnType, e.type) ||
|
|
384
|
+
!callee.params.every((p, i) => typeEquals(p.type, ft.params[i])))
|
|
385
|
+
return null;
|
|
386
|
+
let clone = this.clones.get(callee.name);
|
|
387
|
+
if (clone === undefined) {
|
|
388
|
+
clone = `${callee.name}%direct`;
|
|
389
|
+
this.clones.set(callee.name, clone);
|
|
390
|
+
}
|
|
391
|
+
return clone;
|
|
392
|
+
}
|
|
393
|
+
factsOf(fn) {
|
|
394
|
+
const known = this.facts.get(fn.name);
|
|
395
|
+
if (known !== undefined)
|
|
396
|
+
return known;
|
|
397
|
+
const facts = {
|
|
398
|
+
locals: new Map(fn.locals.map((local) => [local.id, local])),
|
|
399
|
+
declarations: new Map(),
|
|
400
|
+
writes: new Map(),
|
|
401
|
+
hoisted: new Map(),
|
|
402
|
+
callees: new Set(),
|
|
403
|
+
tdzInits: new Map(),
|
|
404
|
+
classCaptured: new Set(),
|
|
405
|
+
reads: new Map(),
|
|
406
|
+
edges: [],
|
|
407
|
+
};
|
|
408
|
+
this.facts.set(fn.name, facts);
|
|
409
|
+
const capturesIn = (e) => {
|
|
410
|
+
const ids = new Set();
|
|
411
|
+
const visitExpr = (node) => {
|
|
412
|
+
if (node.kind === "closure" || node.kind === "classRef")
|
|
413
|
+
for (const id of node.captures ?? [])
|
|
414
|
+
ids.add(id);
|
|
415
|
+
return everyExprChild(node, visitExpr, visitStmt);
|
|
416
|
+
};
|
|
417
|
+
const visitStmt = (node) => everyStmtChild(node, visitExpr, visitStmt);
|
|
418
|
+
visitExpr(e);
|
|
419
|
+
return ids;
|
|
420
|
+
};
|
|
421
|
+
const findHoisted = (body) => {
|
|
422
|
+
body.forEach((s, i) => {
|
|
423
|
+
if (s.kind !== "varDecl" || s.init !== null)
|
|
424
|
+
return;
|
|
425
|
+
for (let j = i + 1; j < body.length; j++) {
|
|
426
|
+
const next = body[j];
|
|
427
|
+
if (next.kind === "assign" && next.value.kind === "closure") {
|
|
428
|
+
if (next.localId === s.localId) {
|
|
429
|
+
facts.hoisted.set(s.localId, next);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (next.kind !== "varDecl" || next.init !== null)
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
const declare = (s, splittable) => {
|
|
439
|
+
const entry = facts.declarations.get(s.localId);
|
|
440
|
+
const initialized = s.init !== null && !capturesIn(s.init).has(s.localId);
|
|
441
|
+
const literal = s.init === null ? null : plainLiteral(constantNumber(s.init));
|
|
442
|
+
if (entry === undefined)
|
|
443
|
+
facts.declarations.set(s.localId, {
|
|
444
|
+
count: 1,
|
|
445
|
+
initialized,
|
|
446
|
+
splittable,
|
|
447
|
+
literal,
|
|
448
|
+
init: s.init,
|
|
449
|
+
});
|
|
450
|
+
else {
|
|
451
|
+
entry.count++;
|
|
452
|
+
entry.initialized = false;
|
|
453
|
+
entry.splittable = false;
|
|
454
|
+
entry.literal = null;
|
|
455
|
+
entry.init = null;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
const write = (id) => {
|
|
459
|
+
facts.writes.set(id, (facts.writes.get(id) ?? 0) + 1);
|
|
460
|
+
};
|
|
461
|
+
const expr = (e) => {
|
|
462
|
+
switch (e.kind) {
|
|
463
|
+
case "varRef":
|
|
464
|
+
facts.reads.set(e.localId, (facts.reads.get(e.localId) ?? 0) + 1);
|
|
465
|
+
break;
|
|
466
|
+
case "assignExpr":
|
|
467
|
+
case "incDec":
|
|
468
|
+
write(e.localId);
|
|
469
|
+
break;
|
|
470
|
+
case "closure": {
|
|
471
|
+
let list = this.sites.get(e.fnName);
|
|
472
|
+
if (list === undefined)
|
|
473
|
+
this.sites.set(e.fnName, (list = []));
|
|
474
|
+
list.push({ owner: fn.name, captures: e.captures, adapts: e.adapts === true });
|
|
475
|
+
e.captures.forEach((source, index) => facts.edges.push({ target: e.fnName, index, source }));
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case "classRef":
|
|
479
|
+
for (const id of e.captures ?? [])
|
|
480
|
+
facts.classCaptured.add(id);
|
|
481
|
+
break;
|
|
482
|
+
case "callValue":
|
|
483
|
+
if (e.receiver === undefined && e.callee.kind === "varRef")
|
|
484
|
+
facts.callees.add(e.callee.localId);
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
return everyExprChild(e, expr, (s) => stmt(s, false));
|
|
488
|
+
};
|
|
489
|
+
// `split` mirrors rewrite(): only statement lists reached through
|
|
490
|
+
// ordinary nesting from the body are expanded. Switch cases share one
|
|
491
|
+
// scope across dispatch targets, `for` heads are freshened per
|
|
492
|
+
// iteration, and statement lists inside expressions release their
|
|
493
|
+
// locals with the enclosing frame; declarations there stay as they are.
|
|
494
|
+
const findTdzInits = (body) => {
|
|
495
|
+
body.forEach((s, j) => {
|
|
496
|
+
if (s.kind !== "assign" || s.initializes !== true)
|
|
497
|
+
return;
|
|
498
|
+
const declared = body.findIndex((d, i) => i < j && d.kind === "varDecl" && d.localId === s.localId && d.init === null);
|
|
499
|
+
if (declared >= 0)
|
|
500
|
+
facts.tdzInits.set(s.localId, { body, declared, initialized: j });
|
|
501
|
+
});
|
|
502
|
+
};
|
|
503
|
+
const list = (body, split) => {
|
|
504
|
+
if (split) {
|
|
505
|
+
findHoisted(body);
|
|
506
|
+
findTdzInits(body);
|
|
507
|
+
}
|
|
508
|
+
return body.every((s) => stmt(s, split));
|
|
509
|
+
};
|
|
510
|
+
const stmt = (s, split) => {
|
|
511
|
+
switch (s.kind) {
|
|
512
|
+
case "varDecl":
|
|
513
|
+
declare(s, split);
|
|
514
|
+
return s.init === null || expr(s.init);
|
|
515
|
+
case "assign":
|
|
516
|
+
case "rethrow":
|
|
517
|
+
write(s.localId);
|
|
518
|
+
break;
|
|
519
|
+
case "forOf":
|
|
520
|
+
write(s.localId);
|
|
521
|
+
return expr(s.iterable) && list(s.body, split);
|
|
522
|
+
case "tryCatch":
|
|
523
|
+
if (s.catchLocalId !== null)
|
|
524
|
+
write(s.catchLocalId);
|
|
525
|
+
return (list(s.tryBody, split) &&
|
|
526
|
+
(s.catchBody === null || list(s.catchBody, split)) &&
|
|
527
|
+
(s.finallyBody === null || list(s.finallyBody, split)));
|
|
528
|
+
case "block":
|
|
529
|
+
return list(s.body, split);
|
|
530
|
+
case "if":
|
|
531
|
+
return expr(s.cond) && list(s.then, split) && (s.else_ === null || list(s.else_, split));
|
|
532
|
+
case "while":
|
|
533
|
+
return expr(s.cond) && list(s.body, split);
|
|
534
|
+
case "doWhile":
|
|
535
|
+
return list(s.body, split) && expr(s.cond);
|
|
536
|
+
case "for":
|
|
537
|
+
return ((s.init === null || stmt(s.init, false)) &&
|
|
538
|
+
(s.cond === null || expr(s.cond)) &&
|
|
539
|
+
(s.update === null || stmt(s.update, false)) &&
|
|
540
|
+
list(s.body, split));
|
|
541
|
+
}
|
|
542
|
+
return everyStmtChild(s, expr, (child) => stmt(child, false));
|
|
543
|
+
};
|
|
544
|
+
list(fn.body, true);
|
|
545
|
+
return facts;
|
|
546
|
+
}
|
|
547
|
+
local(fn, id) {
|
|
548
|
+
return this.factsOf(fn).locals.get(id);
|
|
549
|
+
}
|
|
550
|
+
/** Nothing rebinds `id` in `fnName` (declarations aside, and `ownWrites`
|
|
551
|
+
* writes of its own body) or in any body that shares its box through
|
|
552
|
+
* closure captures. */
|
|
553
|
+
unchanged(fnName, id, ownWrites = 0) {
|
|
554
|
+
const key = `${fnName}\0${id}\0${ownWrites}`;
|
|
555
|
+
const known = this.unchangedMemo.get(key);
|
|
556
|
+
if (known !== undefined)
|
|
557
|
+
return known;
|
|
558
|
+
this.unchangedMemo.set(key, false);
|
|
559
|
+
const fn = this.functions.get(fnName);
|
|
560
|
+
let result = false;
|
|
561
|
+
if (fn !== undefined) {
|
|
562
|
+
const facts = this.factsOf(fn);
|
|
563
|
+
result =
|
|
564
|
+
(facts.writes.get(id) ?? 0) === ownWrites &&
|
|
565
|
+
!facts.classCaptured.has(id) &&
|
|
566
|
+
facts.edges.every((edge) => {
|
|
567
|
+
if (edge.source !== id)
|
|
568
|
+
return true;
|
|
569
|
+
const capture = this.functions.get(edge.target)?.captures?.[edge.index];
|
|
570
|
+
return capture !== undefined && this.unchanged(edge.target, capture.localId);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
this.unchangedMemo.set(key, result);
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
/** A boxed binding of `fn` itself (not a capture) whose box is filled
|
|
577
|
+
* once, before any environment observes it, and never written again. */
|
|
578
|
+
stableOrigin(fn, id) {
|
|
579
|
+
const local = this.local(fn, id);
|
|
580
|
+
if (local?.boxed !== true)
|
|
581
|
+
return false;
|
|
582
|
+
if (local.tdz === true) {
|
|
583
|
+
if (fn.captures?.some((c) => c.localId === id))
|
|
584
|
+
return false;
|
|
585
|
+
if (this.factsOf(fn).declarations.get(id)?.count !== 1)
|
|
586
|
+
return false;
|
|
587
|
+
return this.orderedTdz(fn, id) && this.unchanged(fn.name, id, 1);
|
|
588
|
+
}
|
|
589
|
+
if (fn.captures?.some((c) => c.localId === id))
|
|
590
|
+
return false;
|
|
591
|
+
if (fn.classCaptures?.some((c) => c.localId === id))
|
|
592
|
+
return false;
|
|
593
|
+
const facts = this.factsOf(fn);
|
|
594
|
+
const declaration = facts.declarations.get(id);
|
|
595
|
+
if (fn.params.some((p) => p.localId === id))
|
|
596
|
+
return declaration === undefined && this.unchanged(fn.name, id);
|
|
597
|
+
if (declaration?.count !== 1)
|
|
598
|
+
return false;
|
|
599
|
+
if (declaration.initialized)
|
|
600
|
+
return this.unchanged(fn.name, id);
|
|
601
|
+
return facts.hoisted.has(id) && this.unchanged(fn.name, id, 1);
|
|
602
|
+
}
|
|
603
|
+
/** A closure capture holding one value for the environment's life. */
|
|
604
|
+
stableCapture(fnName, id) {
|
|
605
|
+
const key = `${fnName}\0${id}`;
|
|
606
|
+
const known = this.stableMemo.get(key);
|
|
607
|
+
if (known !== undefined)
|
|
608
|
+
return known;
|
|
609
|
+
this.stableMemo.set(key, false);
|
|
610
|
+
const result = this.computeStableCapture(fnName, id);
|
|
611
|
+
this.stableMemo.set(key, result);
|
|
612
|
+
return result;
|
|
613
|
+
}
|
|
614
|
+
computeStableCapture(fnName, id) {
|
|
615
|
+
const fn = this.functions.get(fnName);
|
|
616
|
+
const index = fn?.captures?.findIndex((c) => c.localId === id) ?? -1;
|
|
617
|
+
if (fn === undefined || index < 0)
|
|
618
|
+
return false;
|
|
619
|
+
const sites = this.sites.get(fnName);
|
|
620
|
+
if (sites === undefined || sites.length === 0)
|
|
621
|
+
return false;
|
|
622
|
+
for (const site of sites) {
|
|
623
|
+
const owner = this.functions.get(site.owner);
|
|
624
|
+
const source = site.captures[index];
|
|
625
|
+
if (owner === undefined || source === undefined || site.adapts)
|
|
626
|
+
return false;
|
|
627
|
+
if (!this.stableOrigin(owner, source) && !this.stableCapture(site.owner, source))
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
return this.unchanged(fnName, id);
|
|
631
|
+
}
|
|
632
|
+
/** The numeric constant every execution binds to `id`, if any. */
|
|
633
|
+
literal(fnName, id, depth = 0) {
|
|
634
|
+
const fn = this.functions.get(fnName);
|
|
635
|
+
if (fn === undefined || depth > 64)
|
|
636
|
+
return null;
|
|
637
|
+
const own = this.factsOf(fn).declarations.get(id);
|
|
638
|
+
if (own !== undefined) {
|
|
639
|
+
if (!this.stableOrigin(fn, id))
|
|
640
|
+
return null;
|
|
641
|
+
const init = this.factsOf(fn).tdzInits.get(id);
|
|
642
|
+
const assign = init?.body[init.initialized];
|
|
643
|
+
return assign?.kind === "assign" ? plainLiteral(constantNumber(assign.value)) : own.literal;
|
|
644
|
+
}
|
|
645
|
+
const index = fn.captures?.findIndex((c) => c.localId === id) ?? -1;
|
|
646
|
+
if (index < 0 || !this.stableCapture(fnName, id))
|
|
647
|
+
return null;
|
|
648
|
+
let value = null;
|
|
649
|
+
for (const site of this.sites.get(fnName) ?? []) {
|
|
650
|
+
const source = site.captures[index];
|
|
651
|
+
const next = source === undefined ? null : this.literal(site.owner, source, depth + 1);
|
|
652
|
+
if (next === null || (value !== null && !Object.is(value, next)))
|
|
653
|
+
return null;
|
|
654
|
+
value = next;
|
|
655
|
+
}
|
|
656
|
+
return value;
|
|
657
|
+
}
|
|
658
|
+
rewrite(fn) {
|
|
659
|
+
const facts = this.factsOf(fn);
|
|
660
|
+
const reads = new Map();
|
|
661
|
+
const splits = new Map();
|
|
662
|
+
const entry = [];
|
|
663
|
+
const locals = [];
|
|
664
|
+
const used = new Set(fn.locals.map((l) => l.id));
|
|
665
|
+
const fresh = (local) => {
|
|
666
|
+
let k = 0;
|
|
667
|
+
while (used.has(`${local.name}.${k}`))
|
|
668
|
+
k++;
|
|
669
|
+
const id = `${local.name}.${k}`;
|
|
670
|
+
used.add(id);
|
|
671
|
+
const copy = { id, name: local.name, type: local.type, mutable: false };
|
|
672
|
+
if (local.source !== undefined)
|
|
673
|
+
copy.source = local.source;
|
|
674
|
+
locals.push(copy);
|
|
675
|
+
return copy;
|
|
676
|
+
};
|
|
677
|
+
const adapted = (this.sites.get(fn.name) ?? []).some((site) => site.adapts);
|
|
678
|
+
// Bindings nothing writes after their only initialization. Marking
|
|
679
|
+
// them immutable lets later analyses (callback resolution, borrowing)
|
|
680
|
+
// treat them as the constants they are.
|
|
681
|
+
const immutable = new Set();
|
|
682
|
+
for (const local of fn.locals) {
|
|
683
|
+
if (local.boxed !== true)
|
|
684
|
+
continue;
|
|
685
|
+
const capture = fn.captures?.some((c) => c.localId === local.id) === true;
|
|
686
|
+
const param = fn.params.some((p) => p.localId === local.id);
|
|
687
|
+
if (capture && local.mutable && this.stableCapture(fn.name, local.id))
|
|
688
|
+
immutable.add(local.id);
|
|
689
|
+
if (!facts.reads.has(local.id))
|
|
690
|
+
continue;
|
|
691
|
+
if (capture) {
|
|
692
|
+
if (adapted || !this.stableCapture(fn.name, local.id))
|
|
693
|
+
continue;
|
|
694
|
+
const copy = fresh(local);
|
|
695
|
+
const literal = local.type.kind === "f64" ? this.literal(fn.name, local.id) : null;
|
|
696
|
+
const init = literal !== null
|
|
697
|
+
? { kind: "numLit", value: literal, type: local.type, loc: fn.loc }
|
|
698
|
+
: { kind: "varRef", localId: local.id, type: local.type, loc: fn.loc };
|
|
699
|
+
entry.push({ kind: "varDecl", localId: copy.id, init, loc: fn.loc });
|
|
700
|
+
reads.set(local.id, copy.id);
|
|
701
|
+
}
|
|
702
|
+
else if (param) {
|
|
703
|
+
// Unchanged captured parameters already keep their value in a plain
|
|
704
|
+
// slot (the backend boxes them lazily); a copy only exposes numbers
|
|
705
|
+
// to range analysis.
|
|
706
|
+
if (local.type.kind !== "f64" || !this.stableOrigin(fn, local.id))
|
|
707
|
+
continue;
|
|
708
|
+
const copy = fresh(local);
|
|
709
|
+
entry.push({
|
|
710
|
+
kind: "varDecl",
|
|
711
|
+
localId: copy.id,
|
|
712
|
+
init: { kind: "varRef", localId: local.id, type: local.type, loc: fn.loc },
|
|
713
|
+
loc: fn.loc,
|
|
714
|
+
});
|
|
715
|
+
reads.set(local.id, copy.id);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
if (!this.stableOrigin(fn, local.id))
|
|
719
|
+
continue;
|
|
720
|
+
const declaration = facts.declarations.get(local.id);
|
|
721
|
+
// Hoisted function and temporal-dead-zone bindings split at their
|
|
722
|
+
// assignment, which is only recorded in expandable statement lists.
|
|
723
|
+
if (declaration.initialized
|
|
724
|
+
? !declaration.splittable
|
|
725
|
+
: !facts.hoisted.has(local.id) && !facts.tdzInits.has(local.id))
|
|
726
|
+
continue;
|
|
727
|
+
const copy = fresh(local);
|
|
728
|
+
splits.set(local.id, copy.id);
|
|
729
|
+
reads.set(local.id, copy.id);
|
|
730
|
+
if (declaration.initialized && local.mutable)
|
|
731
|
+
immutable.add(local.id);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const calls = [...facts.callees].some((id) => this.target(fn.name, id) !== null);
|
|
735
|
+
if (reads.size === 0 && immutable.size === 0 && !calls)
|
|
736
|
+
return fn;
|
|
737
|
+
const localsById = new Map(fn.locals.map((l) => [l.id, l]));
|
|
738
|
+
const expr = (e) => {
|
|
739
|
+
if (e.kind === "varRef") {
|
|
740
|
+
const to = reads.get(e.localId);
|
|
741
|
+
return to === undefined ? e : { ...e, localId: to };
|
|
742
|
+
}
|
|
743
|
+
if (e.kind === "callValue" && e.callee.kind === "varRef") {
|
|
744
|
+
const callee = this.directCall(fn.name, e);
|
|
745
|
+
if (callee !== null)
|
|
746
|
+
return { kind: "call", callee, args: e.args.map(expr), type: e.type, loc: e.loc };
|
|
747
|
+
}
|
|
748
|
+
return mapExprChildren(e, expr, single);
|
|
749
|
+
};
|
|
750
|
+
const single = (s) => mapStmtChildren(s, expr, single);
|
|
751
|
+
const list = (body) => body.flatMap(many);
|
|
752
|
+
const many = (s) => {
|
|
753
|
+
switch (s.kind) {
|
|
754
|
+
case "varDecl": {
|
|
755
|
+
const to = splits.get(s.localId);
|
|
756
|
+
if (to === undefined || s.init === null)
|
|
757
|
+
break;
|
|
758
|
+
const local = localsById.get(s.localId);
|
|
759
|
+
return [
|
|
760
|
+
{ ...s, localId: to, init: expr(s.init) },
|
|
761
|
+
{
|
|
762
|
+
kind: "varDecl",
|
|
763
|
+
localId: s.localId,
|
|
764
|
+
init: { kind: "varRef", localId: to, type: local.type, loc: s.loc },
|
|
765
|
+
loc: s.loc,
|
|
766
|
+
},
|
|
767
|
+
];
|
|
768
|
+
}
|
|
769
|
+
case "assign": {
|
|
770
|
+
const to = splits.get(s.localId);
|
|
771
|
+
const init = facts.tdzInits.get(s.localId);
|
|
772
|
+
const splitHere = facts.hoisted.get(s.localId) === s || init?.body[init.initialized] === s;
|
|
773
|
+
if (to === undefined || !splitHere)
|
|
774
|
+
break;
|
|
775
|
+
const local = localsById.get(s.localId);
|
|
776
|
+
return [
|
|
777
|
+
{ kind: "varDecl", localId: to, init: expr(s.value), loc: s.loc },
|
|
778
|
+
{ ...s, value: { kind: "varRef", localId: to, type: local.type, loc: s.loc } },
|
|
779
|
+
];
|
|
780
|
+
}
|
|
781
|
+
case "block":
|
|
782
|
+
return [{ ...s, body: list(s.body) }];
|
|
783
|
+
case "if":
|
|
784
|
+
return [
|
|
785
|
+
{
|
|
786
|
+
...s,
|
|
787
|
+
cond: expr(s.cond),
|
|
788
|
+
then: list(s.then),
|
|
789
|
+
else_: s.else_ === null ? null : list(s.else_),
|
|
790
|
+
},
|
|
791
|
+
];
|
|
792
|
+
case "while":
|
|
793
|
+
return [{ ...s, cond: expr(s.cond), body: list(s.body) }];
|
|
794
|
+
case "doWhile":
|
|
795
|
+
return [{ ...s, body: list(s.body), cond: expr(s.cond) }];
|
|
796
|
+
case "for":
|
|
797
|
+
return [
|
|
798
|
+
{
|
|
799
|
+
...s,
|
|
800
|
+
init: s.init === null ? null : single(s.init),
|
|
801
|
+
cond: s.cond === null ? null : expr(s.cond),
|
|
802
|
+
update: s.update === null ? null : single(s.update),
|
|
803
|
+
body: list(s.body),
|
|
804
|
+
},
|
|
805
|
+
];
|
|
806
|
+
case "forOf":
|
|
807
|
+
return [{ ...s, iterable: expr(s.iterable), body: list(s.body) }];
|
|
808
|
+
case "tryCatch":
|
|
809
|
+
return [
|
|
810
|
+
{
|
|
811
|
+
...s,
|
|
812
|
+
tryBody: list(s.tryBody),
|
|
813
|
+
catchBody: s.catchBody === null ? null : list(s.catchBody),
|
|
814
|
+
finallyBody: s.finallyBody === null ? null : list(s.finallyBody),
|
|
815
|
+
},
|
|
816
|
+
];
|
|
817
|
+
}
|
|
818
|
+
return [single(s)];
|
|
819
|
+
};
|
|
820
|
+
const kept = fn.locals.map((local) => immutable.has(local.id) ? immutableLocal(local) : local);
|
|
821
|
+
if (reads.size === 0 && !calls)
|
|
822
|
+
return { ...fn, locals: kept };
|
|
823
|
+
const body = list(fn.body);
|
|
824
|
+
// Direct calls can leave an entry copy unread.
|
|
825
|
+
const readIds = new Set();
|
|
826
|
+
everyStmtList(body, {
|
|
827
|
+
stmt: () => true,
|
|
828
|
+
expr: (e) => {
|
|
829
|
+
if (e.kind === "varRef")
|
|
830
|
+
readIds.add(e.localId);
|
|
831
|
+
return true;
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
const live = entry.filter((s) => s.kind !== "varDecl" || readIds.has(s.localId));
|
|
835
|
+
// An unshared function binding whose every call became direct is dead:
|
|
836
|
+
// its definition only creates a closure, which then never exists.
|
|
837
|
+
const dead = new Set([...facts.callees].filter((id) => !readIds.has(id) &&
|
|
838
|
+
facts.locals.get(id)?.boxed !== true &&
|
|
839
|
+
this.target(fn.name, id) !== null));
|
|
840
|
+
const pruned = dead.size === 0 ? body : prune(body, dead);
|
|
841
|
+
const liveIds = new Set(live.map((s) => (s.kind === "varDecl" ? s.localId : "")));
|
|
842
|
+
const added = locals.filter((local) => liveIds.has(local.id) || !entry.some((s) => s.kind === "varDecl" && s.localId === local.id));
|
|
843
|
+
return { ...fn, locals: [...kept, ...added], body: [...live, ...pruned] };
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/** The same local, read-only: nothing writes it after its declaration. */
|
|
847
|
+
function immutableLocal(local) {
|
|
848
|
+
return { ...local, mutable: false };
|
|
849
|
+
}
|
|
850
|
+
//# sourceMappingURL=capture-values.js.map
|