@samitouri / QOS-React-2 / commits / a86d279c46

Initial (flagged) support for reactive scopes with early return

Adds support for early returns within reactive scopes, behind a new feature flag. The flag is off by default, where this case continues to throw a Todo bailout. Since implementing a sketch of the codegen in the previous PR I realized that it's easy enough to implement the more optimal output, so i've updated that here. Rather than both the if and else branch of the reactive scope having an "if the return value was not a sentinel return it" check, we instead make the return temporary a proper declaration of the reactive scope. Then, since it's actually an output it's available in the outer block scope, and we could do a single if-return after the reactive scope. Edit: see comment below for thoughts on test cases.

Joe Savona committed Dec 20, 2023 at 13:52 UTC a86d279c462938c78dd08de5b011fbe6e553546a
24 files changed +960 -358
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+10
@@ -275,6 +275,16 @@ const EnvironmentConfigSchema = z.object({
275 // Enable merging consecutive scopes that invalidate together.
276 enableMergeConsecutiveScopes: z.boolean().default(true),
277
278 + /**
279 + * Enable support for reactive scopes that contain an early return.
280 + * This is relatively infrequent, as reactive scopes generally span
281 + * up to but excluding return statements.
282 + *
283 + * When disabled (default), the compiler will skip any functions which
284 + * would create a reactive scope that contains a return statement.
285 + */
286 + enableEarlyReturnInReactiveScopes: z.boolean().default(false),
287 +
288 // Enable validation of mutable ranges
289 assertValidMutableRanges: z.boolean().default(false),
290
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+2 -2
@@ -1081,9 +1081,9 @@ export type ReactiveScope = {
1081 * This value is null for scopes that do not contain early returns.
1082 */
1083 earlyReturnValue: {
1084 - value: IdentifierId;
1084 + value: Identifier;
1085 loc: SourceLocation;
1086 - label: string;
1086 + label: BlockId;
1087 } | null;
1088
1089 /*
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+18 -81
@@ -452,87 +452,6 @@ function codegenReactiveScope(
452 computationBlock.body.push(...cacheStoreStatements);
453 const memoBlock = t.blockStatement(cacheLoadStatements);
454
455 - if (scope.earlyReturnValue !== null) {
456 - /**
457 - * Handle early return. PropagateEarlyReturns should already have
458 - * converted the actual return statements within the block into
459 - * the appropriate form, so we just have to add the appropriate
460 - * wrapping code.
461 - *
462 - * Example:
463 - *
464 - * ```
465 - * if (input !== $[0]) {
466 - * let t0 = Symbol.for('react.memo_cache_sentinel');
467 - * label: {
468 - * ... memo block ...
469 - * if (cond) {
470 - * // this part is already rewritten by PropagateEarlyReturns
471 - * t0 = ...; // save the early return value
472 - * break label;
473 - * }
474 - * ... more memo block...
475 - * }
476 - * $[1] = t0;
477 - * if (t0 !== Symbol.for('react.memo_cache_sentinel')) {
478 - * return t0;
479 - * }
480 - * } else {
481 - * ...
482 - * const t0 = $[1];
483 - * if (t0 !== Symbol.for('react.memo_cache_sentinel')) {
484 - * return t0;
485 - * }
486 - * }
487 - * ```
488 - *
489 - * TODO: factor out the common if-return from the if/else branches
490 - * We can lift the temporary (here `t0`) to the outer block,
491 - * then move the `if (t0 !== sentinel) { return t0 }` to after the
492 - * memo block if/else, since both branches need to execute that check.
493 - */
494 - const index = cx.nextCacheIndex;
495 - const identifier = t.identifier(`t${cx.env.nextIdentifierId}`);
496 - const sentinel = t.callExpression(
497 - t.memberExpression(t.identifier("Symbol"), t.identifier("for")),
498 - [t.stringLiteral("react.memo_cache_sentinel")]
499 - );
500 - computationBlock = t.blockStatement([
501 - t.variableDeclaration("let", [
502 - t.variableDeclarator(identifier, sentinel),
503 - ]),
504 - t.labeledStatement(
505 - t.identifier(scope.earlyReturnValue.label),
506 - computationBlock
507 - ),
508 - t.expressionStatement(
509 - t.assignmentExpression(
510 - "=",
511 - t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
512 - identifier
513 - )
514 - ),
515 - t.ifStatement(
516 - t.binaryExpression("!==", identifier, sentinel),
517 - t.blockStatement([t.returnStatement(identifier)])
518 - ),
519 - ]);
520 -
521 - memoBlock.body.push(
522 - ...[
523 - t.variableDeclaration("const", [
524 - t.variableDeclarator(
525 - identifier,
526 - t.memberExpression(t.identifier("$"), t.numericLiteral(index), true)
527 - ),
528 - ]),
529 - t.ifStatement(
530 - t.binaryExpression("!==", identifier, sentinel),
531 - t.blockStatement([t.returnStatement(identifier)])
532 - ),
533 - ]
534 - );
535 - }
455 const memoStatement = t.ifStatement(
456 testCondition,
457 computationBlock,
@@ -588,6 +507,24 @@ function codegenReactiveScope(
507 }
508 }
509 statements.push(memoStatement);
510 +
511 + if (scope.earlyReturnValue !== null) {
512 + statements.push(
513 + t.ifStatement(
514 + t.binaryExpression(
515 + "!==",
516 + t.identifier(scope.earlyReturnValue.value.name!),
517 + t.callExpression(
518 + t.memberExpression(t.identifier("Symbol"), t.identifier("for")),
519 + [t.stringLiteral("react.memo_cache_sentinel")]
520 + )
521 + ),
522 + t.blockStatement([
523 + t.returnStatement(t.identifier(scope.earlyReturnValue.value.name!)),
524 + ])
525 + )
526 + );
527 + }
528 }
529
530 function codegenTerminal(
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PropagateEarlyReturns.ts
+254 -46
@@ -6,13 +6,20 @@
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.
@@ -43,78 +50,279 @@ import { ReactiveFunctionVisitor } from "./visitors";
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 }
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneUnusedScopes.ts
+1
@@ -28,6 +28,7 @@ class Transform extends ReactiveFunctionTransform<void> {
28 ): Transformed<ReactiveStatement> {
29 this.visitScope(scopeBlock, state);
30 if (
31 + scopeBlock.scope.earlyReturnValue === null &&
32 scopeBlock.scope.reassignments.size === 0 &&
33 (scopeBlock.scope.declarations.size === 0 ||
34 /*
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-early-return.expect.md new
+203
@@ -0,0 +1,203 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableEarlyReturnInReactiveScopes
6 +
7 +/**
8 + * props.b does *not* influence `a`
9 + */
10 +function ComponentA(props) {
11 + const a_DEBUG = [];
12 + a_DEBUG.push(props.a);
13 + if (props.b) {
14 + return null;
15 + }
16 + a_DEBUG.push(props.d);
17 + return a_DEBUG;
18 +}
19 +
20 +/**
21 + * props.b *does* influence `a`
22 + */
23 +function ComponentB(props) {
24 + const a = [];
25 + a.push(props.a);
26 + if (props.b) {
27 + a.push(props.c);
28 + }
29 + a.push(props.d);
30 + return a;
31 +}
32 +
33 +/**
34 + * props.b *does* influence `a`, but only in a way that is never observable
35 + */
36 +function ComponentC(props) {
37 + const a = [];
38 + a.push(props.a);
39 + if (props.b) {
40 + a.push(props.c);
41 + return null;
42 + }
43 + a.push(props.d);
44 + return a;
45 +}
46 +
47 +/**
48 + * props.b *does* influence `a`
49 + */
50 +function ComponentD(props) {
51 + const a = [];
52 + a.push(props.a);
53 + if (props.b) {
54 + a.push(props.c);
55 + return a;
56 + }
57 + a.push(props.d);
58 + return a;
59 +}
60 +
61 +export const FIXTURE_ENTRYPOINT = {
62 + fn: ComponentA,
63 + params: [{ a: 1, b: false, d: 3 }],
64 +};
65 +
66 +```
67 +
68 +## Code
69 +
70 +```javascript
71 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEarlyReturnInReactiveScopes
72 +
73 +/**
74 + * props.b does *not* influence `a`
75 + */
76 +function ComponentA(props) {
77 + const $ = useMemoCache(5);
78 + let a_DEBUG;
79 + let t37;
80 + if ($[0] !== props.a || $[1] !== props.b || $[2] !== props.d) {
81 + t37 = Symbol.for("react.memo_cache_sentinel");
82 + bb7: {
83 + a_DEBUG = [];
84 + a_DEBUG.push(props.a);
85 + if (props.b) {
86 + t37 = null;
87 + break bb7;
88 + }
89 +
90 + a_DEBUG.push(props.d);
91 + }
92 + $[0] = props.a;
93 + $[1] = props.b;
94 + $[2] = props.d;
95 + $[3] = a_DEBUG;
96 + $[4] = t37;
97 + } else {
98 + a_DEBUG = $[3];
99 + t37 = $[4];
100 + }
101 + if (t37 !== Symbol.for("react.memo_cache_sentinel")) {
102 + return t37;
103 + }
104 + return a_DEBUG;
105 +}
106 +
107 +/**
108 + * props.b *does* influence `a`
109 + */
110 +function ComponentB(props) {
111 + const $ = useMemoCache(2);
112 + let a;
113 + if ($[0] !== props) {
114 + a = [];
115 + a.push(props.a);
116 + if (props.b) {
117 + a.push(props.c);
118 + }
119 +
120 + a.push(props.d);
121 + $[0] = props;
122 + $[1] = a;
123 + } else {
124 + a = $[1];
125 + }
126 + return a;
127 +}
128 +
129 +/**
130 + * props.b *does* influence `a`, but only in a way that is never observable
131 + */
132 +function ComponentC(props) {
133 + const $ = useMemoCache(3);
134 + let a;
135 + let t47;
136 + if ($[0] !== props) {
137 + t47 = Symbol.for("react.memo_cache_sentinel");
138 + bb7: {
139 + a = [];
140 + a.push(props.a);
141 + if (props.b) {
142 + a.push(props.c);
143 + t47 = null;
144 + break bb7;
145 + }
146 +
147 + a.push(props.d);
148 + }
149 + $[0] = props;
150 + $[1] = a;
151 + $[2] = t47;
152 + } else {
153 + a = $[1];
154 + t47 = $[2];
155 + }
156 + if (t47 !== Symbol.for("react.memo_cache_sentinel")) {
157 + return t47;
158 + }
159 + return a;
160 +}
161 +
162 +/**
163 + * props.b *does* influence `a`
164 + */
165 +function ComponentD(props) {
166 + const $ = useMemoCache(3);
167 + let a;
168 + let t47;
169 + if ($[0] !== props) {
170 + t47 = Symbol.for("react.memo_cache_sentinel");
171 + bb7: {
172 + a = [];
173 + a.push(props.a);
174 + if (props.b) {
175 + a.push(props.c);
176 + t47 = a;
177 + break bb7;
178 + }
179 +
180 + a.push(props.d);
181 + }
182 + $[0] = props;
183 + $[1] = a;
184 + $[2] = t47;
185 + } else {
186 + a = $[1];
187 + t47 = $[2];
188 + }
189 + if (t47 !== Symbol.for("react.memo_cache_sentinel")) {
190 + return t47;
191 + }
192 + return a;
193 +}
194 +
195 +export const FIXTURE_ENTRYPOINT = {
196 + fn: ComponentA,
197 + params: [{ a: 1, b: false, d: 3 }],
198 +};
199 +
200 +```
201 +
202 +### Eval output
203 +(kind: ok) [1,3]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-early-return.js renamed
+7
@@ -1,3 +1,5 @@
1 +// @enableEarlyReturnInReactiveScopes
2 +
3 /**
4 * props.b does *not* influence `a`
5 */
@@ -51,3 +53,8 @@ function ComponentD(props) {
53 a.push(props.d);
54 return a;
55 }
56 +
57 +export const FIXTURE_ENTRYPOINT = {
58 + fn: ComponentA,
59 + params: [{ a: 1, b: false, d: 3 }],
60 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/early-return-nested-early-return-within-reactive-scope.expect.md new
+90
@@ -0,0 +1,90 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableEarlyReturnInReactiveScopes
6 +function Component(props) {
7 + let x = [];
8 + if (props.cond) {
9 + x.push(props.a);
10 + if (props.b) {
11 + const y = [props.b];
12 + x.push(y);
13 + // oops no memo!
14 + return x;
15 + }
16 + // oops no memo!
17 + return x;
18 + } else {
19 + return foo();
20 + }
21 +}
22 +
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: Component,
25 + params: [{ cond: true, a: 42, b: 3.14 }],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEarlyReturnInReactiveScopes
34 +function Component(props) {
35 + const $ = useMemoCache(5);
36 + let t53;
37 + if ($[0] !== props) {
38 + t53 = Symbol.for("react.memo_cache_sentinel");
39 + bb11: {
40 + const x = [];
41 + if (props.cond) {
42 + x.push(props.a);
43 + if (props.b) {
44 + let t0;
45 + if ($[2] !== props.b) {
46 + t0 = [props.b];
47 + $[2] = props.b;
48 + $[3] = t0;
49 + } else {
50 + t0 = $[3];
51 + }
52 + const y = t0;
53 + x.push(y);
54 + t53 = x;
55 + break bb11;
56 + }
57 +
58 + t53 = x;
59 + break bb11;
60 + } else {
61 + let t1;
62 + if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
63 + t1 = foo();
64 + $[4] = t1;
65 + } else {
66 + t1 = $[4];
67 + }
68 + t53 = t1;
69 + break bb11;
70 + }
71 + }
72 + $[0] = props;
73 + $[1] = t53;
74 + } else {
75 + t53 = $[1];
76 + }
77 + if (t53 !== Symbol.for("react.memo_cache_sentinel")) {
78 + return t53;
79 + }
80 +}
81 +
82 +export const FIXTURE_ENTRYPOINT = {
83 + fn: Component,
84 + params: [{ cond: true, a: 42, b: 3.14 }],
85 +};
86 +
87 +```
88 +
89 +### Eval output
90 +(kind: ok) [42,[3.14]]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/early-return-nested-early-return-within-reactive-scope.js renamed
+1
@@ -1,3 +1,4 @@
1 +// @enableEarlyReturnInReactiveScopes
2 function Component(props) {
3 let x = [];
4 if (props.cond) {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.expect.md new
+69
@@ -0,0 +1,69 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableEarlyReturnInReactiveScopes
6 +function Component(props) {
7 + let x = [];
8 + if (props.cond) {
9 + x.push(props.a);
10 + // oops no memo!
11 + return x;
12 + } else {
13 + return foo();
14 + }
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{ cond: true, a: 42 }],
20 +};
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEarlyReturnInReactiveScopes
28 +function Component(props) {
29 + const $ = useMemoCache(3);
30 + let t29;
31 + if ($[0] !== props) {
32 + t29 = Symbol.for("react.memo_cache_sentinel");
33 + bb8: {
34 + const x = [];
35 + if (props.cond) {
36 + x.push(props.a);
37 + t29 = x;
38 + break bb8;
39 + } else {
40 + let t0;
41 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
42 + t0 = foo();
43 + $[2] = t0;
44 + } else {
45 + t0 = $[2];
46 + }
47 + t29 = t0;
48 + break bb8;
49 + }
50 + }
51 + $[0] = props;
52 + $[1] = t29;
53 + } else {
54 + t29 = $[1];
55 + }
56 + if (t29 !== Symbol.for("react.memo_cache_sentinel")) {
57 + return t29;
58 + }
59 +}
60 +
61 +export const FIXTURE_ENTRYPOINT = {
62 + fn: Component,
63 + params: [{ cond: true, a: 42 }],
64 +};
65 +
66 +```
67 +
68 +### Eval output
69 +(kind: ok) [42]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.js renamed
+2 -21
@@ -1,35 +1,16 @@
1 -
2 -## Input
3 -
4 -```javascript
1 +// @enableEarlyReturnInReactiveScopes
2 function Component(props) {
3 let x = [];
7 - let y = null;
4 if (props.cond) {
5 x.push(props.a);
6 // oops no memo!
7 return x;
8 } else {
13 - y = foo();
14 - if (props.b) {
15 - return;
16 - }
9 + return foo();
10 }
18 - return y;
11 }
12
13 export const FIXTURE_ENTRYPOINT = {
14 fn: Component,
15 params: [{ cond: true, a: 42 }],
16 };
25 -
26 -```
27 -
28 -
29 -## Error
30 -
31 -```
32 -[ReactForget] Todo: Support early return within a reactive scope (7:7)
33 -```
34 -
35 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-early-return--conditional-break.expect.md deleted
-68
@@ -1,68 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -/**
6 - * props.b does *not* influence `a`
7 - */
8 -function ComponentA(props) {
9 - const a_DEBUG = [];
10 - a_DEBUG.push(props.a);
11 - if (props.b) {
12 - return null;
13 - }
14 - a_DEBUG.push(props.d);
15 - return a_DEBUG;
16 -}
17 -
18 -/**
19 - * props.b *does* influence `a`
20 - */
21 -function ComponentB(props) {
22 - const a = [];
23 - a.push(props.a);
24 - if (props.b) {
25 - a.push(props.c);
26 - }
27 - a.push(props.d);
28 - return a;
29 -}
30 -
31 -/**
32 - * props.b *does* influence `a`, but only in a way that is never observable
33 - */
34 -function ComponentC(props) {
35 - const a = [];
36 - a.push(props.a);
37 - if (props.b) {
38 - a.push(props.c);
39 - return null;
40 - }
41 - a.push(props.d);
42 - return a;
43 -}
44 -
45 -/**
46 - * props.b *does* influence `a`
47 - */
48 -function ComponentD(props) {
49 - const a = [];
50 - a.push(props.a);
51 - if (props.b) {
52 - a.push(props.c);
53 - return a;
54 - }
55 - a.push(props.d);
56 - return a;
57 -}
58 -
59 -```
60 -
61 -
62 -## Error
63 -
64 -```
65 -[ReactForget] Todo: Support early return within a reactive scope (8:8)
66 -```
67 -
68 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-early-return--early-return-nested-early-return-within-reactive-scope.expect.md deleted
-36
@@ -1,36 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Component(props) {
6 - let x = [];
7 - if (props.cond) {
8 - x.push(props.a);
9 - if (props.b) {
10 - const y = [props.b];
11 - x.push(y);
12 - // oops no memo!
13 - return x;
14 - }
15 - // oops no memo!
16 - return x;
17 - } else {
18 - return foo();
19 - }
20 -}
21 -
22 -export const FIXTURE_ENTRYPOINT = {
23 - fn: Component,
24 - params: [{ cond: true, a: 42, b: 3.14 }],
25 -};
26 -
27 -```
28 -
29 -
30 -## Error
31 -
32 -```
33 -[ReactForget] Todo: Support early return within a reactive scope (9:9)
34 -```
35 -
36 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-early-return--try-catch-try-value-modified-in-catch.expect.md deleted
-33
@@ -1,33 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -const { throwInput } = require("shared-runtime");
6 -
7 -function Component(props) {
8 - try {
9 - const y = [];
10 - y.push(props.y);
11 - throwInput(y);
12 - } catch (e) {
13 - e.push(props.e);
14 - return e;
15 - }
16 - return null;
17 -}
18 -
19 -export const FIXTURE_ENTRYPOINT = {
20 - fn: Component,
21 - params: [{ y: "foo", e: "bar" }],
22 -};
23 -
24 -```
25 -
26 -
27 -## Error
28 -
29 -```
30 -[ReactForget] Todo: Support early return within a reactive scope (10:10)
31 -```
32 -
33 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-early-return--try-catch-with-catch-param.expect.md deleted
-34
@@ -1,34 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -const { throwInput } = require("shared-runtime");
6 -
7 -function Component(props) {
8 - let x = [];
9 - try {
10 - // foo could throw its argument...
11 - throwInput(x);
12 - } catch (e) {
13 - // ... in which case this could be mutating `x`!
14 - e.push(null);
15 - return e;
16 - }
17 - return x;
18 -}
19 -
20 -export const FIXTURE_ENTRYPOINT = {
21 - fn: Component,
22 - params: [{}],
23 -};
24 -
25 -```
26 -
27 -
28 -## Error
29 -
30 -```
31 -[ReactForget] Todo: Support early return within a reactive scope (11:11)
32 -```
33 -
34 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-early-return--try-catch-with-return.expect.md deleted
-36
@@ -1,36 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -const { shallowCopy, throwInput } = require("shared-runtime");
6 -
7 -// @debug
8 -function Component(props) {
9 - let x = [];
10 - try {
11 - const y = shallowCopy({});
12 - if (y == null) {
13 - return;
14 - }
15 - x.push(throwInput(y));
16 - } catch {
17 - return null;
18 - }
19 - return x;
20 -}
21 -
22 -export const FIXTURE_ENTRYPOINT = {
23 - fn: Component,
24 - params: [{}],
25 -};
26 -
27 -```
28 -
29 -
30 -## Error
31 -
32 -```
33 -[ReactForget] Todo: Support early return within a reactive scope
34 -```
35 -
36 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/partial-early-return-within-reactive-scope.expect.md new
+81
@@ -0,0 +1,81 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableEarlyReturnInReactiveScopes
6 +function Component(props) {
7 + let x = [];
8 + let y = null;
9 + if (props.cond) {
10 + x.push(props.a);
11 + // oops no memo!
12 + return x;
13 + } else {
14 + y = foo();
15 + if (props.b) {
16 + return;
17 + }
18 + }
19 + return y;
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Component,
24 + params: [{ cond: true, a: 42 }],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEarlyReturnInReactiveScopes
33 +function Component(props) {
34 + const $ = useMemoCache(4);
35 + let y;
36 + let t46;
37 + if ($[0] !== props) {
38 + t46 = Symbol.for("react.memo_cache_sentinel");
39 + bb11: {
40 + const x = [];
41 + if (props.cond) {
42 + x.push(props.a);
43 + t46 = x;
44 + break bb11;
45 + } else {
46 + let t0;
47 + if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
48 + t0 = foo();
49 + $[3] = t0;
50 + } else {
51 + t0 = $[3];
52 + }
53 + y = t0;
54 + if (props.b) {
55 + t46 = undefined;
56 + break bb11;
57 + }
58 + }
59 + }
60 + $[0] = props;
61 + $[1] = y;
62 + $[2] = t46;
63 + } else {
64 + y = $[1];
65 + t46 = $[2];
66 + }
67 + if (t46 !== Symbol.for("react.memo_cache_sentinel")) {
68 + return t46;
69 + }
70 + return y;
71 +}
72 +
73 +export const FIXTURE_ENTRYPOINT = {
74 + fn: Component,
75 + params: [{ cond: true, a: 42 }],
76 +};
77 +
78 +```
79 +
80 +### Eval output
81 +(kind: ok) [42]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/partial-early-return-within-reactive-scope.js renamed
+1
@@ -1,3 +1,4 @@
1 +// @enableEarlyReturnInReactiveScopes
2 function Component(props) {
3 let x = [];
4 let y = null;
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-try-value-modified-in-catch.expect.md new
+72
@@ -0,0 +1,72 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableEarlyReturnInReactiveScopes
6 +const { throwInput } = require("shared-runtime");
7 +
8 +function Component(props) {
9 + try {
10 + const y = [];
11 + y.push(props.y);
12 + throwInput(y);
13 + } catch (e) {
14 + e.push(props.e);
15 + return e;
16 + }
17 + return null;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{ y: "foo", e: "bar" }],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEarlyReturnInReactiveScopes
31 +const { throwInput } = require("shared-runtime");
32 +
33 +function Component(props) {
34 + const $ = useMemoCache(3);
35 + let t49;
36 + if ($[0] !== props.y || $[1] !== props.e) {
37 + t49 = Symbol.for("react.memo_cache_sentinel");
38 + bb18: {
39 + try {
40 + const y = [];
41 + y.push(props.y);
42 + throwInput(y);
43 + } catch (t25) {
44 + const e = t25;
45 + e.push(props.e);
46 + t49 = e;
47 + break bb18;
48 + }
49 +
50 + t49 = null;
51 + break bb18;
52 + }
53 + $[0] = props.y;
54 + $[1] = props.e;
55 + $[2] = t49;
56 + } else {
57 + t49 = $[2];
58 + }
59 + if (t49 !== Symbol.for("react.memo_cache_sentinel")) {
60 + return t49;
61 + }
62 +}
63 +
64 +export const FIXTURE_ENTRYPOINT = {
65 + fn: Component,
66 + params: [{ y: "foo", e: "bar" }],
67 +};
68 +
69 +```
70 +
71 +### Eval output
72 +(kind: ok) ["foo","bar"]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-try-value-modified-in-catch.js renamed
+1
@@ -1,3 +1,4 @@
1 +// @enableEarlyReturnInReactiveScopes
2 const { throwInput } = require("shared-runtime");
3
4 function Component(props) {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-with-catch-param.expect.md new
+70
@@ -0,0 +1,70 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableEarlyReturnInReactiveScopes
6 +const { throwInput } = require("shared-runtime");
7 +
8 +function Component(props) {
9 + let x = [];
10 + try {
11 + // foo could throw its argument...
12 + throwInput(x);
13 + } catch (e) {
14 + // ... in which case this could be mutating `x`!
15 + e.push(null);
16 + return e;
17 + }
18 + return x;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [{}],
24 +};
25 +
26 +```
27 +
28 +## Code
29 +
30 +```javascript
31 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEarlyReturnInReactiveScopes
32 +const { throwInput } = require("shared-runtime");
33 +
34 +function Component(props) {
35 + const $ = useMemoCache(1);
36 + let t36;
37 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
38 + t36 = Symbol.for("react.memo_cache_sentinel");
39 + bb11: {
40 + const x = [];
41 + try {
42 + throwInput(x);
43 + } catch (t22) {
44 + const e = t22;
45 + e.push(null);
46 + t36 = e;
47 + break bb11;
48 + }
49 +
50 + t36 = x;
51 + break bb11;
52 + }
53 + $[0] = t36;
54 + } else {
55 + t36 = $[0];
56 + }
57 + if (t36 !== Symbol.for("react.memo_cache_sentinel")) {
58 + return t36;
59 + }
60 +}
61 +
62 +export const FIXTURE_ENTRYPOINT = {
63 + fn: Component,
64 + params: [{}],
65 +};
66 +
67 +```
68 +
69 +### Eval output
70 +(kind: ok) [null]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-with-catch-param.js renamed
+1
@@ -1,3 +1,4 @@
1 +// @enableEarlyReturnInReactiveScopes
2 const { throwInput } = require("shared-runtime");
3
4 function Component(props) {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-with-return.expect.md new
+76
@@ -0,0 +1,76 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableEarlyReturnInReactiveScopes
6 +const { shallowCopy, throwInput } = require("shared-runtime");
7 +
8 +function Component(props) {
9 + let x = [];
10 + try {
11 + const y = shallowCopy({});
12 + if (y == null) {
13 + return;
14 + }
15 + x.push(throwInput(y));
16 + } catch {
17 + return null;
18 + }
19 + return x;
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Component,
24 + params: [{}],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEarlyReturnInReactiveScopes
33 +const { shallowCopy, throwInput } = require("shared-runtime");
34 +
35 +function Component(props) {
36 + const $ = useMemoCache(2);
37 + let x;
38 + let t43;
39 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
40 + t43 = Symbol.for("react.memo_cache_sentinel");
41 + bb25: {
42 + x = [];
43 + try {
44 + const y = shallowCopy({});
45 + if (y == null) {
46 + t43 = undefined;
47 + break bb25;
48 + }
49 +
50 + x.push(throwInput(y));
51 + } catch {
52 + t43 = null;
53 + break bb25;
54 + }
55 + }
56 + $[0] = x;
57 + $[1] = t43;
58 + } else {
59 + x = $[0];
60 + t43 = $[1];
61 + }
62 + if (t43 !== Symbol.for("react.memo_cache_sentinel")) {
63 + return t43;
64 + }
65 + return x;
66 +}
67 +
68 +export const FIXTURE_ENTRYPOINT = {
69 + fn: Component,
70 + params: [{}],
71 +};
72 +
73 +```
74 +
75 +### Eval output
76 +(kind: ok) null
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-with-return.js renamed
+1 -1
@@ -1,6 +1,6 @@
1 +// @enableEarlyReturnInReactiveScopes
2 const { shallowCopy, throwInput } = require("shared-runtime");
3
3 -// @debug
4 function Component(props) {
5 let x = [];
6 try {