[λ] Store and define captured bindings as context
This lets us treat global references differently from captured references.
Sathya Gunasekaran committed
Feb 2, 2023 at 14:05 UTC
0522d5233bc1bd5a98ba5c266bfa4c08dc7eaa72
11 files changed
+179
-82
compiler/forget/src/HIR/BuildHIR.ts
+22
-12
@@ -49,10 +49,21 @@ import HIRBuilder, { Environment } from "./HIRBuilder";
49
* grained reactivity.
50
*/
51
export function lower(
52
- func: NodePath<t.Function>
52
+ func: NodePath<t.Function>,
53
+ capturedRefs?: t.Identifier[]
54
): Result<HIRFunction, CompilerError> {
55
const env = new Environment();
56
const builder = new HIRBuilder(env);
57
+ const context: Place[] = [];
58
+
59
+ for (const ref of capturedRefs ?? []) {
60
+ context.push({
61
+ kind: "Identifier",
62
+ identifier: builder.resolveBinding(ref),
63
+ effect: Effect.Unknown,
64
+ loc: GeneratedSource,
65
+ });
66
+ }
67
68
// Internal babel is on an older version that does not have hasNode (v7.17)
69
// See https://github.com/babel/babel/pull/13940/files for impl
@@ -125,6 +136,7 @@ export function lower(
136
id,
137
params,
138
body: builder.build(),
139
+ context,
140
generator: func.node.generator === true,
141
async: func.node.async === true,
142
loc: func.node.loc ?? GeneratedSource,
@@ -1300,12 +1312,8 @@ function lowerExpression(
1312
name = expr.get("id")?.node?.name ?? null;
1313
}
1314
const componentScope: Scope = expr.scope.parent.getFunctionParent()!;
1303
- const dependencies: Array<Place> = gatherCapturedDeps(
1304
- builder,
1305
- expr,
1306
- componentScope
1307
- );
1308
- const lowering = lower(expr);
1315
+ const captured = gatherCapturedDeps(builder, expr, componentScope);
1316
+ const lowering = lower(expr, captured.identifiers);
1317
let loweredFunc: HIRFunction;
1318
if (lowering.isErr()) {
1319
lowering
@@ -1340,7 +1348,7 @@ function lowerExpression(
1348
name,
1349
params,
1350
loweredFunc,
1343
- dependencies,
1351
+ dependencies: captured.refs,
1352
mutatedDeps: [],
1353
expr: expr.node,
1354
loc: exprLoc,
@@ -1919,8 +1927,9 @@ function gatherCapturedDeps(
1927
builder: HIRBuilder,
1928
fn: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
1929
componentScope: Scope
1922
-): Array<Place> {
1923
- const captured: Set<Place> = new Set();
1930
+): { identifiers: t.Identifier[]; refs: Place[] } {
1931
+ const capturedIds: Set<t.Identifier> = new Set();
1932
+ const capturedRefs: Set<Place> = new Set();
1933
1934
// Capture all the scopes from the parent of this function up to and including
1935
// the component scope.
@@ -1946,9 +1955,10 @@ function gatherCapturedDeps(
1955
}
1956
1957
path.skip();
1949
- captured.add(lowerExpressionToPlace(builder, path));
1958
+ capturedIds.add(binding.identifier);
1959
+ capturedRefs.add(lowerExpressionToPlace(builder, path));
1960
},
1961
});
1962
1953
- return [...captured];
1963
+ return { identifiers: [...capturedIds], refs: [...capturedRefs] };
1964
}
compiler/forget/src/HIR/HIR.ts
+1
@@ -184,6 +184,7 @@ export type HIRFunction = {
184
id: Identifier | null;
185
env: Environment;
186
params: Array<Place>;
187
+ context: Array<Place>;
188
body: HIR;
189
generator: boolean;
190
async: boolean;
compiler/forget/src/HIR/HIRBuilder.ts
+5
@@ -172,6 +172,11 @@ export default class HIRBuilder {
172
const node =
173
path.scope.getBindingIdentifier(originalName) ??
174
getOrAddGlobal(originalName);
175
+ return this.resolveBinding(node);
176
+ }
177
+
178
+ resolveBinding(node: t.Identifier) {
179
+ const originalName = node.name;
180
let name = originalName;
181
let index = 0;
182
while (true) {
compiler/forget/src/Inference/AnalyseFunctions.ts
+4
-16
@@ -1,11 +1,4 @@
1
-import {
2
- Effect,
3
- HIRFunction,
4
- Identifier,
5
- mergeConsecutiveBlocks,
6
- Place,
7
-} from "../HIR";
8
-import { eachInstructionOperand } from "../HIR/visitors";
1
+import { HIRFunction, Identifier, mergeConsecutiveBlocks, Place } from "../HIR";
2
import { constantPropagation } from "../Optimization";
3
import { eliminateRedundantPhi, enterSSA } from "../SSA";
4
import { inferTypes } from "../TypeInference";
@@ -116,18 +109,13 @@ function analyzeMutatedPlaces(func: HIRFunction): Array<Place> {
109
) {
110
mutations.push(...analyzeMutatedPlaces(instr.value.loweredFunc));
111
}
119
-
120
- for (const operand of eachInstructionOperand(instr)) {
121
- if (isMutated(operand)) {
122
- mutations.push(operand);
123
- }
124
- }
112
}
113
}
114
115
+ mutations.push(...func.context.filter((dep) => isMutated(dep.identifier)));
116
return mutations;
117
}
118
131
-function isMutated(place: Place): boolean {
132
- return place.effect === Effect.Mutate || place.effect === Effect.Store;
119
+function isMutated(id: Identifier) {
120
+ return id.mutableRange.end - id.mutableRange.start > 1;
121
}
compiler/forget/src/Inference/InferReferenceEffects.ts
+11
@@ -93,6 +93,17 @@ export default function inferReferenceEffects(fn: HIRFunction) {
93
initialEnvironment.define(id, value);
94
}
95
96
+ for (const ref of fn.context) {
97
+ // TODO(gsn): This is a hack.
98
+ const value: InstructionValue = {
99
+ kind: "ObjectExpression",
100
+ properties: null,
101
+ loc: ref.loc,
102
+ };
103
+ initialEnvironment.initialize(value, ValueKind.Mutable);
104
+ initialEnvironment.define(ref, value);
105
+ }
106
+
107
for (const param of fn.params) {
108
const value: InstructionValue = {
109
kind: "Primitive",
compiler/forget/src/SSA/EnterSSA.ts
+1
@@ -195,6 +195,7 @@ export default function enterSSA(func: HIRFunction) {
195
builder.startBlock(block);
196
197
if (func.body.entry === blockId) {
198
+ func.context = func.context.map((p) => builder.definePlace(p));
199
func.params = func.params.map((p) => builder.definePlace(p));
200
}
201
compiler/forget/src/__tests__/fixtures/hir/capture-param-mutate.expect.md
new
+97
@@ -0,0 +1,97 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function getNativeLogFunction(level) {
6
+ return function () {
7
+ let str;
8
+ if (arguments.length === 1 && typeof arguments[0] === "string") {
9
+ str = arguments[0];
10
+ } else {
11
+ str = Array.prototype.map
12
+ .call(arguments, function (arg) {
13
+ return inspect(arg, {
14
+ depth: 10,
15
+ });
16
+ })
17
+ .join(", ");
18
+ }
19
+ const firstArg = arguments[0];
20
+ let logLevel = level;
21
+ if (
22
+ typeof firstArg === "string" &&
23
+ firstArg.slice(0, 9) === "Warning: " &&
24
+ logLevel >= LOG_LEVELS.error
25
+ ) {
26
+ logLevel = LOG_LEVELS.warn;
27
+ }
28
+ if (global.__inspectorLog) {
29
+ global.__inspectorLog(
30
+ INSPECTOR_LEVELS[logLevel],
31
+ str,
32
+ [].slice.call(arguments),
33
+ INSPECTOR_FRAMES_TO_SKIP
34
+ );
35
+ }
36
+ if (groupStack.length) {
37
+ str = groupFormat("", str);
38
+ }
39
+ global.nativeLoggingHook(str, logLevel);
40
+ };
41
+}
42
+
43
+```
44
+
45
+## Code
46
+
47
+```javascript
48
+function getNativeLogFunction(level) {
49
+ const $ = React.useMemoCache();
50
+ const c_0 = $[0] !== level;
51
+ let t1;
52
+ if (c_0) {
53
+ t1 = function () {
54
+ let str;
55
+ if (arguments.length === 1 && typeof arguments[0] === "string") {
56
+ str = arguments[0];
57
+ } else {
58
+ str = Array.prototype.map
59
+ .call(arguments, function (arg) {
60
+ return inspect(arg, {
61
+ depth: 10,
62
+ });
63
+ })
64
+ .join(", ");
65
+ }
66
+ const firstArg = arguments[0];
67
+ let logLevel = level;
68
+ if (
69
+ typeof firstArg === "string" &&
70
+ firstArg.slice(0, 9) === "Warning: " &&
71
+ logLevel >= LOG_LEVELS.error
72
+ ) {
73
+ logLevel = LOG_LEVELS.warn;
74
+ }
75
+ if (global.__inspectorLog) {
76
+ global.__inspectorLog(
77
+ INSPECTOR_LEVELS[logLevel],
78
+ str,
79
+ [].slice.call(arguments),
80
+ INSPECTOR_FRAMES_TO_SKIP
81
+ );
82
+ }
83
+ if (groupStack.length) {
84
+ str = groupFormat("", str);
85
+ }
86
+ global.nativeLoggingHook(str, logLevel);
87
+ };
88
+ $[0] = level;
89
+ $[1] = t1;
90
+ } else {
91
+ t1 = $[1];
92
+ }
93
+ return t1;
94
+}
95
+
96
+```
97
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/capture-param-mutate.js
renamed
compiler/forget/src/__tests__/fixtures/hir/capturing-func-mutate-nested.expect.md
new
+38
@@ -0,0 +1,38 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function component(a) {
6
+ let y = { b: { a } };
7
+ let x = function () {
8
+ y.b.a = 2;
9
+ };
10
+ x();
11
+ return x;
12
+}
13
+
14
+```
15
+
16
+## Code
17
+
18
+```javascript
19
+function component(a) {
20
+ const $ = React.useMemoCache();
21
+ const c_0 = $[0] !== a;
22
+ let x;
23
+ if (c_0) {
24
+ const y = { b: { a: a } };
25
+ x = function () {
26
+ y.b.a = 2;
27
+ };
28
+ x();
29
+ $[0] = a;
30
+ $[1] = x;
31
+ } else {
32
+ x = $[1];
33
+ }
34
+ return x;
35
+}
36
+
37
+```
38
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/capturing-func-mutate-nested.js
-2
@@ -1,5 +1,3 @@
1
-// @skip
2
-// TODO(gsn): This doesn't seem to work correctly. Need to debug more.
1
function component(a) {
2
let y = { b: { a } };
3
let x = function () {
compiler/forget/src/__tests__/fixtures/hir/error._bug_fbsource_pokes_infer_reference_effects.expect.md
deleted
-52
@@ -1,52 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-function getNativeLogFunction(level) {
6
- return function () {
7
- let str;
8
- if (arguments.length === 1 && typeof arguments[0] === "string") {
9
- str = arguments[0];
10
- } else {
11
- str = Array.prototype.map
12
- .call(arguments, function (arg) {
13
- return inspect(arg, {
14
- depth: 10,
15
- });
16
- })
17
- .join(", ");
18
- }
19
- const firstArg = arguments[0];
20
- let logLevel = level;
21
- if (
22
- typeof firstArg === "string" &&
23
- firstArg.slice(0, 9) === "Warning: " &&
24
- logLevel >= LOG_LEVELS.error
25
- ) {
26
- logLevel = LOG_LEVELS.warn;
27
- }
28
- if (global.__inspectorLog) {
29
- global.__inspectorLog(
30
- INSPECTOR_LEVELS[logLevel],
31
- str,
32
- [].slice.call(arguments),
33
- INSPECTOR_FRAMES_TO_SKIP
34
- );
35
- }
36
- if (groupStack.length) {
37
- str = groupFormat("", str);
38
- }
39
- global.nativeLoggingHook(str, logLevel);
40
- };
41
-}
42
-
43
-```
44
-
45
-
46
-## Error
47
-
48
-```
49
-[ReactForget] Invariant: InferReferenceEffects::kind: Expected at least one value at '<unknown> logLevel$85:TPrimitive' (20:20)
50
-```
51
-
52
-
\ No newline at end of file