Skip to content

Commit 0d07288

Browse files
authored
[compiler] Inferred effect dependencies now include optional chains (#33326)
Inferred effect dependencies now include optional chains. This is a temporary solution while #32099 and its followups are worked on. Ideally, we should model reactive scope dependencies in the IR similarly to `ComputeIR` -- dependencies should be hoisted and all references rewritten to use the hoisted dependencies. ` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33326). * __->__ #33326 * #33325 * #32286
1 parent abf9fd5 commit 0d07288

12 files changed

+762
-115
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import {
2+
Place,
3+
ReactiveScopeDependency,
4+
Identifier,
5+
makeInstructionId,
6+
InstructionKind,
7+
GeneratedSource,
8+
BlockId,
9+
makeTemporaryIdentifier,
10+
Effect,
11+
GotoVariant,
12+
HIR,
13+
} from './HIR';
14+
import {CompilerError} from '../CompilerError';
15+
import {Environment} from './Environment';
16+
import HIRBuilder from './HIRBuilder';
17+
import {lowerValueToTemporary} from './BuildHIR';
18+
19+
type DependencyInstructions = {
20+
place: Place;
21+
value: HIR;
22+
exitBlockId: BlockId;
23+
};
24+
25+
export function buildDependencyInstructions(
26+
dep: ReactiveScopeDependency,
27+
env: Environment,
28+
): DependencyInstructions {
29+
const builder = new HIRBuilder(env, {
30+
entryBlockKind: 'value',
31+
});
32+
let dependencyValue: Identifier;
33+
if (dep.path.every(path => !path.optional)) {
34+
dependencyValue = writeNonOptionalDependency(dep, env, builder);
35+
} else {
36+
dependencyValue = writeOptionalDependency(dep, builder, null);
37+
}
38+
39+
const exitBlockId = builder.terminate(
40+
{
41+
kind: 'unsupported',
42+
loc: GeneratedSource,
43+
id: makeInstructionId(0),
44+
},
45+
null,
46+
);
47+
return {
48+
place: {
49+
kind: 'Identifier',
50+
identifier: dependencyValue,
51+
effect: Effect.Freeze,
52+
reactive: dep.reactive,
53+
loc: GeneratedSource,
54+
},
55+
value: builder.build(),
56+
exitBlockId,
57+
};
58+
}
59+
60+
/**
61+
* Write instructions for a simple dependency (without optional chains)
62+
*/
63+
function writeNonOptionalDependency(
64+
dep: ReactiveScopeDependency,
65+
env: Environment,
66+
builder: HIRBuilder,
67+
): Identifier {
68+
const loc = dep.identifier.loc;
69+
let curr: Identifier = makeTemporaryIdentifier(env.nextIdentifierId, loc);
70+
builder.push({
71+
lvalue: {
72+
identifier: curr,
73+
kind: 'Identifier',
74+
effect: Effect.Mutate,
75+
reactive: dep.reactive,
76+
loc,
77+
},
78+
value: {
79+
kind: 'LoadLocal',
80+
place: {
81+
identifier: dep.identifier,
82+
kind: 'Identifier',
83+
effect: Effect.Freeze,
84+
reactive: dep.reactive,
85+
loc,
86+
},
87+
loc,
88+
},
89+
id: makeInstructionId(1),
90+
loc: loc,
91+
});
92+
93+
/**
94+
* Iteratively build up dependency instructions by reading from the last written
95+
* instruction.
96+
*/
97+
for (const path of dep.path) {
98+
const next = makeTemporaryIdentifier(env.nextIdentifierId, loc);
99+
builder.push({
100+
lvalue: {
101+
identifier: next,
102+
kind: 'Identifier',
103+
effect: Effect.Mutate,
104+
reactive: dep.reactive,
105+
loc,
106+
},
107+
value: {
108+
kind: 'PropertyLoad',
109+
object: {
110+
identifier: curr,
111+
kind: 'Identifier',
112+
effect: Effect.Freeze,
113+
reactive: dep.reactive,
114+
loc,
115+
},
116+
property: path.property,
117+
loc,
118+
},
119+
id: makeInstructionId(1),
120+
loc: loc,
121+
});
122+
curr = next;
123+
}
124+
return curr;
125+
}
126+
127+
/**
128+
* Write a dependency into optional blocks.
129+
*
130+
* e.g. `a.b?.c.d` is written to an optional block that tests `a.b` and
131+
* conditionally evaluates `c.d`.
132+
*/
133+
function writeOptionalDependency(
134+
dep: ReactiveScopeDependency,
135+
builder: HIRBuilder,
136+
parentAlternate: BlockId | null,
137+
): Identifier {
138+
const env = builder.environment;
139+
/**
140+
* Reserve an identifier which will be used to store the result of this
141+
* dependency.
142+
*/
143+
const dependencyValue: Place = {
144+
kind: 'Identifier',
145+
identifier: makeTemporaryIdentifier(env.nextIdentifierId, GeneratedSource),
146+
effect: Effect.Mutate,
147+
reactive: dep.reactive,
148+
loc: GeneratedSource,
149+
};
150+
151+
/**
152+
* Reserve a block which is the fallthrough (and transitive successor) of this
153+
* optional chain.
154+
*/
155+
const continuationBlock = builder.reserve(builder.currentBlockKind());
156+
let alternate;
157+
if (parentAlternate != null) {
158+
alternate = parentAlternate;
159+
} else {
160+
/**
161+
* If an outermost alternate block has not been reserved, write one
162+
*
163+
* $N = Primitive undefined
164+
* $M = StoreLocal $OptionalResult = $N
165+
* goto fallthrough
166+
*/
167+
alternate = builder.enter('value', () => {
168+
const temp = lowerValueToTemporary(builder, {
169+
kind: 'Primitive',
170+
value: undefined,
171+
loc: GeneratedSource,
172+
});
173+
lowerValueToTemporary(builder, {
174+
kind: 'StoreLocal',
175+
lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
176+
value: {...temp},
177+
type: null,
178+
loc: GeneratedSource,
179+
});
180+
return {
181+
kind: 'goto',
182+
variant: GotoVariant.Break,
183+
block: continuationBlock.id,
184+
id: makeInstructionId(0),
185+
loc: GeneratedSource,
186+
};
187+
});
188+
}
189+
190+
// Reserve the consequent block, which is the successor of the test block
191+
const consequent = builder.reserve('value');
192+
193+
let testIdentifier: Identifier | null = null;
194+
const testBlock = builder.enter('value', () => {
195+
const testDependency = {
196+
...dep,
197+
path: dep.path.slice(0, dep.path.length - 1),
198+
};
199+
const firstOptional = dep.path.findIndex(path => path.optional);
200+
CompilerError.invariant(firstOptional !== -1, {
201+
reason:
202+
'[ScopeDependencyUtils] Internal invariant broken: expected optional path',
203+
loc: dep.identifier.loc,
204+
description: null,
205+
suggestions: null,
206+
});
207+
if (firstOptional === dep.path.length - 1) {
208+
// Base case: the test block is simple
209+
testIdentifier = writeNonOptionalDependency(testDependency, env, builder);
210+
} else {
211+
// Otherwise, the test block is a nested optional chain
212+
testIdentifier = writeOptionalDependency(
213+
testDependency,
214+
builder,
215+
alternate,
216+
);
217+
}
218+
219+
return {
220+
kind: 'branch',
221+
test: {
222+
identifier: testIdentifier,
223+
effect: Effect.Freeze,
224+
kind: 'Identifier',
225+
loc: GeneratedSource,
226+
reactive: dep.reactive,
227+
},
228+
consequent: consequent.id,
229+
alternate,
230+
id: makeInstructionId(0),
231+
loc: GeneratedSource,
232+
fallthrough: continuationBlock.id,
233+
};
234+
});
235+
236+
builder.enterReserved(consequent, () => {
237+
CompilerError.invariant(testIdentifier !== null, {
238+
reason: 'Satisfy type checker',
239+
description: null,
240+
loc: null,
241+
suggestions: null,
242+
});
243+
244+
lowerValueToTemporary(builder, {
245+
kind: 'StoreLocal',
246+
lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
247+
value: lowerValueToTemporary(builder, {
248+
kind: 'PropertyLoad',
249+
object: {
250+
identifier: testIdentifier,
251+
kind: 'Identifier',
252+
effect: Effect.Freeze,
253+
reactive: dep.reactive,
254+
loc: GeneratedSource,
255+
},
256+
property: dep.path.at(-1)!.property,
257+
loc: GeneratedSource,
258+
}),
259+
type: null,
260+
loc: GeneratedSource,
261+
});
262+
return {
263+
kind: 'goto',
264+
variant: GotoVariant.Break,
265+
block: continuationBlock.id,
266+
id: makeInstructionId(0),
267+
loc: GeneratedSource,
268+
};
269+
});
270+
builder.terminateWithContinuation(
271+
{
272+
kind: 'optional',
273+
optional: dep.path.at(-1)!.optional,
274+
test: testBlock,
275+
fallthrough: continuationBlock.id,
276+
id: makeInstructionId(0),
277+
loc: GeneratedSource,
278+
},
279+
continuationBlock,
280+
);
281+
282+
return dependencyValue.identifier;
283+
}

0 commit comments

Comments
 (0)