6
*/
7
8
import { visitReactiveFunction } from ".";
9
-import { CompilerError } from "..";
9
+import { CompilerError, Effect } from "..";
10
import {
11
+ Environment,
12
+ InstructionKind,
13
ReactiveFunction,
14
+ ReactiveScope,
15
ReactiveScopeBlock,
16
+ ReactiveStatement,
17
ReactiveTerminalStatement,
18
+ makeInstructionId,
19
+ makeType,
20
} from "../HIR";
15
-import { ReactiveFunctionVisitor } from "./visitors";
21
+import { createTemporaryPlace } from "../HIR/HIRBuilder";
22
+import { ReactiveFunctionTransform, Transformed } from "./visitors";
23
24
/**
25
* TODO: Actualy propagate early return information, for now we throw a Todo bailout.
50
* - Label the scope
51
* - Synthesize a new temporary, eg `t0`, and set it as a declaration of the scope.
52
* This will represent the possibly-unset return value for that scope.
46
- * - Make the first instruction of the scope a reassignment of that temporary,
53
+ * - Make the first instruction of the scope the declaration of that temporary,
54
* assigning a sentinel value (can reuse the same symbol as we use for cache slots).
55
* This assignment ensures that if we don't take an early return, that the value
56
* is the sentinel.
57
* - Replace all `return` statements with:
58
* - An assignment of the temporary with the value being returned.
52
- * - An assignment of the temporary into a cache slot, so it can be retrieved in the
53
- * scope's "else" branch.
59
* - A `break` to the reactive scope's label.
55
- * - Finally, add code _after_ the reactive scope that checks the temporary. If
56
- * it equals the sentinel value do nothing; else return its value.
60
+ *
61
+ * Finally, CodegenReactiveScope adds an if check following the reactive scope:
62
+ * if the early return temporary value is *not* the sentinel value, we early return
63
+ * it. Otherwise, execution continues.
64
*
65
* For the above example that looks roughly like:
66
*
60
- * ```javascript
61
- * let t0; // temporary for early return;
62
- * bb1: if (props.cond !== $[0]) {
63
- * // reset the temporary
64
- * t0 = Symbol.for('react.forget');
65
- * // original code
66
- * let x = [];
67
- * if (props.cond) {
68
- * x.push(12);
69
- * // replace the early return w assignment and break
70
- * t0 = x;
71
- * $[2] = t0;
72
- * break bb1
73
- * } else {
74
- * let t1;
75
- * if ($[1] === Symbol.for('react.forget')) {
76
- * t1 = foo();
77
- * $[1] = t1;
67
+ * ```
68
+ * let t0;
69
+ * if (props.cond !== $[0]) {
70
+ * t0 = Symbol.for('react.memo_cache_sentinel');
71
+ * bb0: {
72
+ * let x = [];
73
+ * if (props.cond) {
74
+ * x.push(12);
75
+ * t0 = x;
76
+ * break bb0;
77
* } else {
79
- * t1 = $[1];
78
+ * let t1;
79
+ * if ($[1] === Symbol.for('react.memo_cache_sentinel')) {
80
+ * t1 = foo();
81
+ * $[1] = t1;
82
+ * } else {
83
+ * t1 = $[1];
84
+ * }
85
+ * t0 = t1;
86
+ * break bb0;
87
* }
81
- * // Replace early return w assignment and break;
82
- * t0 = t1;
83
- * $[2] = t0;
84
- * break bb1;
88
* }
89
+ * $[0] = props.cond;
90
+ * $[2] = t0;
91
* } else {
92
* t0 = $[2];
93
* }
89
- * if (t0 !== Symbol.for('react.forget')) {
94
+ * // This part added in CodegenReactiveScope:
95
+ * if (t0 !== Symbol.for('react.memo_cache_sentinel')) {
96
* return t0;
97
* }
98
* ```
99
*/
100
export function propagateEarlyReturns(fn: ReactiveFunction): void {
95
- visitReactiveFunction(fn, new Visitor(), false);
101
+ visitReactiveFunction(fn, new Transform(fn.env), {
102
+ withinReactiveScope: false,
103
+ earlyReturnValue: null,
104
+ });
105
}
106
98
-class Visitor extends ReactiveFunctionVisitor<boolean> {
107
+type State = {
108
+ /**
109
+ * Are we within a reactive scope? We use this for two things:
110
+ * - When we find an early return, transform it to an assign+break
111
+ * only if we're in a reactive scope
112
+ * - Annotate reactive scopes that contain early returns...but only
113
+ * the outermost reactive scope, we can't do this for nested
114
+ * scopes.
115
+ */
116
+ withinReactiveScope: boolean;
117
+
118
+ /**
119
+ * Store early return information to bubble it back up to the outermost
120
+ * reactive scope
121
+ */
122
+ earlyReturnValue: ReactiveScope["earlyReturnValue"];
123
+};
124
+
125
+class Transform extends ReactiveFunctionTransform<State> {
126
+ env: Environment;
127
+ constructor(env: Environment) {
128
+ super();
129
+ this.env = env;
130
+ }
131
+
132
override visitScope(
133
scopeBlock: ReactiveScopeBlock,
101
- _withinReactiveScope: boolean
134
+ parentState: State
135
): void {
103
- this.traverseScope(scopeBlock, true);
136
+ const innerState: State = {
137
+ withinReactiveScope: true,
138
+ earlyReturnValue: parentState.earlyReturnValue,
139
+ };
140
+ this.traverseScope(scopeBlock, innerState);
141
+
142
+ const earlyReturnValue = innerState.earlyReturnValue;
143
+ if (earlyReturnValue !== null) {
144
+ if (!parentState.withinReactiveScope) {
145
+ // This is the outermost scope wrapping an early return, store the early return information
146
+ scopeBlock.scope.earlyReturnValue = earlyReturnValue;
147
+ scopeBlock.scope.declarations.set(earlyReturnValue.value.id, {
148
+ identifier: earlyReturnValue.value,
149
+ scope: scopeBlock.scope,
150
+ });
151
+
152
+ const instructions = scopeBlock.instructions;
153
+ const loc = earlyReturnValue.loc;
154
+ const sentinelTemp = createTemporaryPlace(this.env);
155
+ const symbolTemp = createTemporaryPlace(this.env);
156
+ const forTemp = createTemporaryPlace(this.env);
157
+ const argTemp = createTemporaryPlace(this.env);
158
+ scopeBlock.instructions = [
159
+ {
160
+ kind: "instruction",
161
+ instruction: {
162
+ id: makeInstructionId(0),
163
+ loc,
164
+ lvalue: { ...symbolTemp },
165
+ value: {
166
+ kind: "LoadGlobal",
167
+ name: "Symbol",
168
+ loc,
169
+ },
170
+ },
171
+ },
172
+ {
173
+ kind: "instruction",
174
+ instruction: {
175
+ id: makeInstructionId(0),
176
+ loc,
177
+ lvalue: { ...forTemp },
178
+ value: {
179
+ kind: "PropertyLoad",
180
+ object: { ...symbolTemp },
181
+ property: "for",
182
+ loc,
183
+ },
184
+ },
185
+ },
186
+ {
187
+ kind: "instruction",
188
+ instruction: {
189
+ id: makeInstructionId(0),
190
+ loc,
191
+ lvalue: { ...argTemp },
192
+ value: {
193
+ kind: "Primitive",
194
+ value: "react.memo_cache_sentinel",
195
+ loc,
196
+ },
197
+ },
198
+ },
199
+ {
200
+ kind: "instruction",
201
+ instruction: {
202
+ id: makeInstructionId(0),
203
+ loc,
204
+ lvalue: { ...sentinelTemp },
205
+ value: {
206
+ kind: "MethodCall",
207
+ receiver: symbolTemp,
208
+ property: forTemp,
209
+ args: [argTemp],
210
+ loc,
211
+ },
212
+ },
213
+ },
214
+ {
215
+ kind: "instruction",
216
+ instruction: {
217
+ id: makeInstructionId(0),
218
+ loc,
219
+ lvalue: null,
220
+ value: {
221
+ kind: "StoreLocal",
222
+ loc,
223
+ type: makeType(),
224
+ lvalue: {
225
+ kind: InstructionKind.Let,
226
+ place: {
227
+ kind: "Identifier",
228
+ effect: Effect.ConditionallyMutate,
229
+ loc,
230
+ reactive: true,
231
+ identifier: earlyReturnValue.value,
232
+ },
233
+ },
234
+ value: { ...sentinelTemp },
235
+ },
236
+ },
237
+ },
238
+ {
239
+ kind: "terminal",
240
+ label: earlyReturnValue.label,
241
+ terminal: {
242
+ kind: "label",
243
+ id: makeInstructionId(0),
244
+ block: instructions,
245
+ },
246
+ },
247
+ ];
248
+ } else {
249
+ /*
250
+ * Not the outermost scope, but we save the early return information in case there are other
251
+ * early returns within the same outermost scope
252
+ */
253
+ parentState.earlyReturnValue = earlyReturnValue;
254
+ }
255
+ }
256
}
257
106
- override visitTerminal(
258
+ override transformTerminal(
259
stmt: ReactiveTerminalStatement,
108
- withinReactiveScope: boolean
109
- ): void {
110
- if (withinReactiveScope && stmt.terminal.kind === "return") {
111
- CompilerError.throwTodo({
112
- reason: `Support early return within a reactive scope`,
113
- loc: stmt.terminal.value.loc,
114
- description: null,
115
- suggestions: null,
116
- });
260
+ state: State
261
+ ): Transformed<ReactiveStatement> {
262
+ if (state.withinReactiveScope && stmt.terminal.kind === "return") {
263
+ if (!this.env.config.enableEarlyReturnInReactiveScopes) {
264
+ CompilerError.throwTodo({
265
+ reason: `Support early return within a reactive scope`,
266
+ loc: stmt.terminal.value.loc,
267
+ description: null,
268
+ suggestions: null,
269
+ });
270
+ }
271
+ const loc = stmt.terminal.value.loc;
272
+ let earlyReturnValue: ReactiveScope["earlyReturnValue"];
273
+ if (state.earlyReturnValue !== null) {
274
+ earlyReturnValue = state.earlyReturnValue;
275
+ } else {
276
+ const identifier = createTemporaryPlace(this.env).identifier;
277
+ identifier.name = `t${identifier.id}`;
278
+ earlyReturnValue = {
279
+ label: this.env.nextBlockId,
280
+ loc,
281
+ value: identifier,
282
+ };
283
+ }
284
+ state.earlyReturnValue = earlyReturnValue;
285
+ return {
286
+ kind: "replace-many",
287
+ value: [
288
+ {
289
+ kind: "instruction",
290
+ instruction: {
291
+ id: makeInstructionId(0),
292
+ loc,
293
+ lvalue: null,
294
+ value: {
295
+ kind: "StoreLocal",
296
+ loc,
297
+ type: makeType(),
298
+ lvalue: {
299
+ kind: InstructionKind.Reassign,
300
+ place: {
301
+ kind: "Identifier",
302
+ identifier: earlyReturnValue.value,
303
+ effect: Effect.Capture,
304
+ loc,
305
+ reactive: true,
306
+ },
307
+ },
308
+ value: stmt.terminal.value,
309
+ },
310
+ },
311
+ },
312
+ {
313
+ kind: "terminal",
314
+ label: null,
315
+ terminal: {
316
+ kind: "break",
317
+ id: makeInstructionId(0),
318
+ implicit: false,
319
+ label: earlyReturnValue.label,
320
+ },
321
+ },
322
+ ],
323
+ };
324
}
118
- this.traverseTerminal(stmt, withinReactiveScope);
325
+ this.traverseTerminal(stmt, state);
326
+ return { kind: "keep" };
327
}
328
}