@samitouri / QOS-React / commits / 5166869204

[patch] Fix control flow bug in PropagateScopeDeps

A dependency D from either an instruction or scope is poisoned if there may be a (non-linear) jump instruction between it and the start of its immediate parent scope. Poisoned dependencies are added as conditional dependencies to their parent scope. (done: reduce false positives in scopes that begin after return/throw) (done: fix bugs in recording and joining exhaustive conditional deps) (done: flesh out commit message, clean up PR, add more fixtures) --- \## Bug details: Take a simple example: ```js target: { instrA; if (...) { instrB; break target; } else { instrC; } instrD; // ... } instrE; // ... ``` This diagram shows how we represent this program in the reactive IR. - Blocks are represented as a list of nodes. - Green nodes show instructions and value blocks (simplified as a single instruction). - Pink nodes show terminals, which transfer control to a subtree of nodes. <img width="450" alt="image" src="https://github.com/facebook/react-forget/assets/34200447/930789f2-39cd-4ea8-b12a-530042807b46"> Prior to this PR, PropagateReactiveScopeDeps was incorrect because it assumed that a block's instructions are evaluated unconditionally (which is how HIR basic blocks work). E.g. if a reactive scope enclosed `block 1`, we assume that `instrA` and `instrD` both will evaluate unconditionally. This failed to account for `jump` instructions like break, continue, return, and throw. This may result in invalid hoisting of PropertyLoads (i.e. Forget output may throw when source does not throw). Note that other terminals (e.g. if and loops) are not affected as they are self contained subtrees that evaluate sequentially. With the changes in this PR, we mark `block 1` as poisoned upon encountering the `break` instruction. While `block 1` is active and poisoned, it will determine how visited dependencies are added. Here, added solid lines show unconditional dependencies, dashed lines show conditionally accessed dependencies: - dependencies from `instrB, instrC` are conditional because they are within conditional subtrees - dependencies from `instrD` are conditional because it is within a poisoned block within its parent scope. <img width="450" alt="image" src="https://github.com/facebook/react-forget/assets/34200447/81980f68-7e65-4bd7-ba94-3f0c26550e5c"> --- Recapping an offline discussion with @josephsavona: this pass would really benefit from operating on HIR. The minimal work needed for this pass to run on HIR is to rewrite and reorder `AlignReactiveScopesToBlockScopes` to operate on HIR. The following diagram shows what HIR blocks look like for the same code. Evaluating hoistable PropertyLoad dependencies for a scope enclosing `instr{A-D}` is much simpler: just evaluate whether the PropertyLoad evaluates for every path between `bb0` and `bb4`. <img width="250" alt="image" src="https://github.com/facebook/react-forget/assets/34200447/44b38939-defb-4b29-878d-4445ec6ccc06"> ---

Mofei Zhang committed Mar 27, 2024 at 20:26 UTC 51668692046ebd997d93c45a91f7cc1ca1479fac
34 files changed +1929 -44
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PropagateScopeDependencies.ts
+266 -16
@@ -7,6 +7,8 @@
7
8 import { CompilerError } from "../CompilerError";
9 import {
10 + BlockId,
11 + GeneratedSource,
12 Identifier,
13 IdentifierId,
14 InstructionId,
@@ -135,9 +137,152 @@ class FindPromotedTemporaries extends ReactiveFunctionVisitor<TemporariesUsedOut
137 type DeclMap = Map<IdentifierId, Decl>;
138 type Decl = {
139 id: InstructionId;
138 - scope: Stack<ReactiveScope>;
140 + scope: Stack<ScopeTraversalState>;
141 };
142
143 +/**
144 + * TraversalState and PoisonState is used to track the poisoned state of a scope.
145 + *
146 + * A scope is poisoned when either of these conditions hold:
147 + * - one of its own nested blocks is a jump target (for break/continues)
148 + * - it is a outermost scope and contains a throw / return
149 + *
150 + * When a scope is poisoned, all dependencies (from instructions and inner scopes)
151 + * are added as conditionally accessed.
152 + */
153 +type ScopeTraversalState = {
154 + value: ReactiveScope;
155 + ownBlocks: Stack<BlockId>;
156 +};
157 +
158 +class PoisonState {
159 + poisonedBlocks: Set<BlockId> = new Set();
160 + poisonedScopes: Set<ScopeId> = new Set();
161 + isPoisoned: boolean = false;
162 +
163 + constructor(
164 + poisonedBlocks: Set<BlockId>,
165 + poisonedScopes: Set<ScopeId>,
166 + isPoisoned: boolean
167 + ) {
168 + this.poisonedBlocks = poisonedBlocks;
169 + this.poisonedScopes = poisonedScopes;
170 + this.isPoisoned = isPoisoned;
171 + }
172 +
173 + clone(): PoisonState {
174 + return new PoisonState(
175 + new Set(this.poisonedBlocks),
176 + new Set(this.poisonedScopes),
177 + this.isPoisoned
178 + );
179 + }
180 +
181 + take(other: PoisonState): PoisonState {
182 + const copy = new PoisonState(
183 + this.poisonedBlocks,
184 + this.poisonedScopes,
185 + this.isPoisoned
186 + );
187 + this.poisonedBlocks = other.poisonedBlocks;
188 + this.poisonedScopes = other.poisonedScopes;
189 + this.isPoisoned = other.isPoisoned;
190 + return copy;
191 + }
192 +
193 + merge(
194 + others: Array<PoisonState>,
195 + currentScope: ScopeTraversalState | null
196 + ): void {
197 + for (const other of others) {
198 + for (const id of other.poisonedBlocks) {
199 + this.poisonedBlocks.add(id);
200 + }
201 + for (const id of other.poisonedScopes) {
202 + this.poisonedScopes.add(id);
203 + }
204 + }
205 + this.#invalidate(currentScope);
206 + }
207 +
208 + #invalidate(currentScope: ScopeTraversalState | null): void {
209 + if (currentScope != null) {
210 + if (this.poisonedScopes.has(currentScope.value.id)) {
211 + this.isPoisoned = true;
212 + return;
213 + } else if (
214 + currentScope.ownBlocks.find((blockId) =>
215 + this.poisonedBlocks.has(blockId)
216 + )
217 + ) {
218 + this.isPoisoned = true;
219 + return;
220 + }
221 + }
222 + this.isPoisoned = false;
223 + }
224 +
225 + /**
226 + * Mark a block or scope as poisoned and update the `isPoisoned` flag.
227 + *
228 + * @param targetBlock id of the block which ends non-linear control flow.
229 + * For a break/continue instruction, this is the target block.
230 + * Throw and return instructions have no target and will poison the earliest
231 + * active scope
232 + */
233 + addPoisonTarget(
234 + target: BlockId | null,
235 + activeScopes: Stack<ScopeTraversalState>
236 + ): void {
237 + const currentScope = activeScopes.value;
238 + if (target == null && currentScope != null) {
239 + let cursor = activeScopes;
240 + while (true) {
241 + const next = cursor.pop();
242 + if (next.value == null) {
243 + const poisonedScope = cursor.value!.value.id;
244 + this.poisonedScopes.add(poisonedScope);
245 + if (poisonedScope === currentScope?.value.id) {
246 + this.isPoisoned = true;
247 + }
248 + break;
249 + } else {
250 + cursor = next;
251 + }
252 + }
253 + } else if (target != null) {
254 + this.poisonedBlocks.add(target);
255 + if (
256 + !this.isPoisoned &&
257 + currentScope?.ownBlocks.find((blockId) => blockId === target)
258 + ) {
259 + this.isPoisoned = true;
260 + }
261 + }
262 + }
263 +
264 + /**
265 + * Invoked during traversal when a poisoned scope becomes inactive
266 + * @param id
267 + * @param currentScope
268 + */
269 + removeMaybePoisonedScope(
270 + id: ScopeId,
271 + currentScope: ScopeTraversalState | null
272 + ): void {
273 + this.poisonedScopes.delete(id);
274 + this.#invalidate(currentScope);
275 + }
276 +
277 + removeMaybePoisonedBlock(
278 + id: BlockId,
279 + currentScope: ScopeTraversalState | null
280 + ): void {
281 + this.poisonedBlocks.delete(id);
282 + this.#invalidate(currentScope);
283 + }
284 +}
285 +
286 class Context {
287 #temporariesUsedOutsideScope: Set<IdentifierId>;
288 #declarations: DeclMap = new Map();
@@ -163,7 +308,8 @@ class Context {
308 */
309 #depsInCurrentConditional: ReactiveScopeDependencyTree =
310 new ReactiveScopeDependencyTree();
166 - #scopes: Stack<ReactiveScope> = empty();
311 + #scopes: Stack<ScopeTraversalState> = empty();
312 + poisonState: PoisonState = new PoisonState(new Set(), new Set(), false);
313
314 constructor(temporariesUsedOutsideScope: Set<IdentifierId>) {
315 this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
@@ -173,6 +319,13 @@ class Context {
319 // Save context of previous scope
320 const prevInConditional = this.#inConditionalWithinScope;
321 const previousDependencies = this.#dependencies;
322 + const prevDepsInConditional: ReactiveScopeDependencyTree | null = this
323 + .isPoisoned
324 + ? this.#depsInCurrentConditional
325 + : null;
326 + if (prevDepsInConditional != null) {
327 + this.#depsInCurrentConditional = new ReactiveScopeDependencyTree();
328 + }
329
330 /*
331 * Set context for new scope
@@ -183,12 +336,18 @@ class Context {
336 const scopedDependencies = new ReactiveScopeDependencyTree();
337 this.#inConditionalWithinScope = false;
338 this.#dependencies = scopedDependencies;
186 - this.#scopes = this.#scopes.push(scope);
339 + this.#scopes = this.#scopes.push({
340 + value: scope,
341 + ownBlocks: empty(),
342 + });
343 + this.poisonState.isPoisoned = false;
344
345 fn();
346
347 // Restore context of previous scope
348 this.#scopes = this.#scopes.pop();
349 + this.poisonState.removeMaybePoisonedScope(scope.id, this.#scopes.value);
350 +
351 this.#dependencies = previousDependencies;
352 this.#inConditionalWithinScope = prevInConditional;
353
@@ -204,9 +363,20 @@ class Context {
363 */
364 this.#dependencies.addDepsFromInnerScope(
365 scopedDependencies,
207 - this.#inConditionalWithinScope,
366 + this.#inConditionalWithinScope || this.isPoisoned,
367 this.#checkValidDependency.bind(this)
368 );
369 +
370 + if (prevDepsInConditional != null) {
371 + // Outer scope is poisoned
372 + prevDepsInConditional.addDepsFromInnerScope(
373 + this.#depsInCurrentConditional,
374 + true,
375 + this.#checkValidDependency.bind(this)
376 + );
377 + this.#depsInCurrentConditional = prevDepsInConditional;
378 + }
379 +
380 return minInnerScopeDependencies;
381 }
382
@@ -368,13 +538,13 @@ class Context {
538 const currentDeclaration =
539 this.#reassignments.get(identifier) ??
540 this.#declarations.get(identifier.id);
371 - const currentScope = this.#scopes !== null ? this.#scopes.value : null;
541 + const currentScope = this.currentScope.value?.value;
542 return (
543 currentScope != null &&
544 currentDeclaration !== undefined &&
545 currentDeclaration.id < currentScope.range.start &&
546 (currentDeclaration.scope == null ||
377 - currentDeclaration.scope.value !== currentScope)
547 + currentDeclaration.scope.value?.value !== currentScope)
548 );
549 }
550
@@ -382,13 +552,17 @@ class Context {
552 if (this.#scopes === null) {
553 return false;
554 }
385 - return this.#scopes.contains(scope);
555 + return this.#scopes.find((state) => state.value === scope);
556 }
557
388 - get currentScope(): Stack<ReactiveScope> {
558 + get currentScope(): Stack<ScopeTraversalState> {
559 return this.#scopes;
560 }
561
562 + get isPoisoned(): boolean {
563 + return this.poisonState.isPoisoned;
564 + }
565 +
566 visitOperand(place: Place): void {
567 const resolved = this.resolveTemporary(place);
568 /*
@@ -436,22 +610,26 @@ class Context {
610 originalDeclaration.scope.value !== null
611 ) {
612 originalDeclaration.scope.each((scope) => {
439 - if (!this.#isScopeActive(scope)) {
440 - scope.declarations.set(maybeDependency.identifier.id, {
613 + if (!this.#isScopeActive(scope.value)) {
614 + scope.value.declarations.set(maybeDependency.identifier.id, {
615 identifier: maybeDependency.identifier,
442 - scope: originalDeclaration.scope.value!, // checked above
616 + scope: originalDeclaration.scope.value!.value,
617 });
618 }
619 });
620 }
621
622 if (this.#checkValidDependency(maybeDependency)) {
449 - this.#depsInCurrentConditional.add(maybeDependency, false);
623 + const isPoisoned = this.isPoisoned;
624 + this.#depsInCurrentConditional.add(maybeDependency, isPoisoned);
625 /*
626 * Add info about this dependency to the existing tree
627 * We do not try to join/reduce dependencies here due to missing info
628 */
454 - this.#dependencies.add(maybeDependency, this.#inConditionalWithinScope);
629 + this.#dependencies.add(
630 + maybeDependency,
631 + this.#inConditionalWithinScope || isPoisoned
632 + );
633 }
634 }
635
@@ -460,15 +638,36 @@ class Context {
638 * current one as a {@link ReactiveScope.reassignments}
639 */
640 visitReassignment(place: Place): void {
641 + const currentScope = this.currentScope.value?.value;
642 if (
464 - this.currentScope.value != null &&
465 - !Array.from(this.currentScope.value.reassignments).some(
643 + currentScope != null &&
644 + !Array.from(currentScope.reassignments).some(
645 (identifier) => identifier.id === place.identifier.id
646 ) &&
647 this.#checkValidDependency({ identifier: place.identifier, path: [] })
648 ) {
470 - this.currentScope.value.reassignments.add(place.identifier);
649 + currentScope.reassignments.add(place.identifier);
650 + }
651 + }
652 +
653 + pushLabeledBlock(id: BlockId): void {
654 + const currentScope = this.#scopes.value;
655 + if (currentScope != null) {
656 + currentScope.ownBlocks = currentScope.ownBlocks.push(id);
657 + }
658 + }
659 + popLabeledBlock(id: BlockId): void {
660 + const currentScope = this.#scopes.value;
661 + if (currentScope != null) {
662 + const last = currentScope.ownBlocks.value;
663 + currentScope.ownBlocks = currentScope.ownBlocks.pop();
664 +
665 + CompilerError.invariant(last != null && last === id, {
666 + reason: "[PropagateScopeDependencies] Misformed block stack",
667 + loc: GeneratedSource,
668 + });
669 }
670 + this.poisonState.removeMaybePoisonedBlock(id, currentScope);
671 }
672 }
673
@@ -659,10 +858,38 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
858 }
859 }
860
861 + enterTerminal(stmt: ReactiveTerminalStatement, context: Context): void {
862 + if (stmt.label != null) {
863 + context.pushLabeledBlock(stmt.label.id);
864 + }
865 + const terminal = stmt.terminal;
866 + switch (terminal.kind) {
867 + case "continue":
868 + case "break": {
869 + context.poisonState.addPoisonTarget(
870 + terminal.target,
871 + context.currentScope
872 + );
873 + break;
874 + }
875 + case "throw":
876 + case "return": {
877 + context.poisonState.addPoisonTarget(null, context.currentScope);
878 + break;
879 + }
880 + }
881 + }
882 + exitTerminal(stmt: ReactiveTerminalStatement, context: Context): void {
883 + if (stmt.label != null) {
884 + context.popLabeledBlock(stmt.label.id);
885 + }
886 + }
887 +
888 override visitTerminal(
889 stmt: ReactiveTerminalStatement,
890 context: Context
891 ): void {
892 + this.enterTerminal(stmt, context);
893 const terminal = stmt.terminal;
894 switch (terminal.kind) {
895 case "break":
@@ -719,13 +946,23 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
946 case "if": {
947 context.visitOperand(terminal.test);
948 const { consequent, alternate } = terminal;
949 + /*
950 + * Consequent and alternate branches are mutually exclusive,
951 + * so we save and restore the poison state here.
952 + */
953 + const prevPoisonState = context.poisonState.clone();
954 const depsInIf = context.enterConditional(() => {
955 this.visitBlock(consequent, context);
956 });
957 if (alternate !== null) {
958 + const ifPoisonState = context.poisonState.take(prevPoisonState);
959 const depsInElse = context.enterConditional(() => {
960 this.visitBlock(alternate, context);
961 });
962 + context.poisonState.merge(
963 + [ifPoisonState],
964 + context.currentScope.value
965 + );
966 context.promoteDepsFromExhaustiveConditionals([depsInIf, depsInElse]);
967 }
968 break;
@@ -743,6 +980,11 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
980 }
981 const depsInCases = [];
982 let foundDefault = false;
983 + /**
984 + * Switch branches are mutually exclusive
985 + */
986 + const prevPoisonState = context.poisonState.clone();
987 + const mutExPoisonStates: Array<PoisonState> = [];
988 /*
989 * This can underestimate unconditional accesses due to the current
990 * CFG representation for fallthrough. This is safe. It only
@@ -755,6 +997,9 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
997 foundDefault = true;
998 }
999 if (block !== undefined) {
1000 + mutExPoisonStates.push(
1001 + context.poisonState.take(prevPoisonState.clone())
1002 + );
1003 depsInCases.push(
1004 context.enterConditional(() => {
1005 this.visitBlock(block, context);
@@ -765,6 +1010,10 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
1010 if (foundDefault) {
1011 context.promoteDepsFromExhaustiveConditionals(depsInCases);
1012 }
1013 + context.poisonState.merge(
1014 + mutExPoisonStates,
1015 + context.currentScope.value
1016 + );
1017 break;
1018 }
1019 case "label": {
@@ -783,5 +1032,6 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
1032 );
1033 }
1034 }
1035 + this.exitTerminal(stmt, context);
1036 }
1037 }
compiler/packages/babel-plugin-react-forget/src/Utils/Stack.ts
+18
@@ -25,10 +25,13 @@ interface StackInterface<T> {
25 pop(): StackInterface<T>;
26
27 contains(value: T): boolean;
28 + find(fn: (value: T) => boolean): boolean;
29
30 each(fn: (value: T) => void): void;
31
32 get value(): T | null;
33 +
34 + print(fn: (node: T) => string): string;
35 }
36
37 export function create<T>(value: T): Stack<T> {
@@ -56,6 +59,10 @@ class Node<T> implements StackInterface<T> {
59 return this.#next;
60 }
61
62 + find(fn: (value: T) => boolean): boolean {
63 + return fn(this.#value) ? true : this.#next.find(fn);
64 + }
65 +
66 contains(value: T): boolean {
67 return (
68 value === this.#value ||
@@ -70,6 +77,10 @@ class Node<T> implements StackInterface<T> {
77 get value(): T {
78 return this.#value;
79 }
80 +
81 + print(fn: (node: T) => string): string {
82 + return fn(this.#value) + this.#next.print(fn);
83 + }
84 }
85
86 class Empty<T> implements StackInterface<T> {
@@ -79,6 +90,10 @@ class Empty<T> implements StackInterface<T> {
90 pop(): Stack<T> {
91 return this;
92 }
93 +
94 + find(_fn: (value: T) => boolean): boolean {
95 + return false;
96 + }
97 contains(_value: T): boolean {
98 return false;
99 }
@@ -88,6 +103,9 @@ class Empty<T> implements StackInterface<T> {
103 get value(): T | null {
104 return null;
105 }
106 + print(_: (node: T) => string): string {
107 + return "";
108 + }
109 }
110
111 const EMPTY: Stack<void> = new Empty();
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md
+5 -13
@@ -34,14 +34,9 @@ import { unstable_useMemoCache as useMemoCache } from "react";
34 * props.b *does* influence `a`
35 */
36 function Component(props) {
37 - const $ = useMemoCache(5);
37 + const $ = useMemoCache(2);
38 let a;
39 - if (
40 - $[0] !== props.a ||
41 - $[1] !== props.b ||
42 - $[2] !== props.c ||
43 - $[3] !== props.d
44 - ) {
39 + if ($[0] !== props) {
40 a = [];
41 a.push(props.a);
42 bb1: {
@@ -53,13 +48,10 @@ function Component(props) {
48 }
49
50 a.push(props.d);
56 - $[0] = props.a;
57 - $[1] = props.b;
58 - $[2] = props.c;
59 - $[3] = props.d;
60 - $[4] = a;
51 + $[0] = props;
52 + $[1] = a;
53 } else {
62 - a = $[4];
54 + a = $[1];
55 }
56 return a;
57 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-early-return.expect.md
+7 -9
@@ -71,10 +71,10 @@ import { unstable_useMemoCache as useMemoCache } from "react";
71 * props.b does *not* influence `a`
72 */
73 function ComponentA(props) {
74 - const $ = useMemoCache(5);
74 + const $ = useMemoCache(3);
75 let a_DEBUG;
76 let t0;
77 - if ($[0] !== props.a || $[1] !== props.b || $[2] !== props.d) {
77 + if ($[0] !== props) {
78 t0 = Symbol.for("react.early_return_sentinel");
79 bb7: {
80 a_DEBUG = [];
@@ -86,14 +86,12 @@ function ComponentA(props) {
86
87 a_DEBUG.push(props.d);
88 }
89 - $[0] = props.a;
90 - $[1] = props.b;
91 - $[2] = props.d;
92 - $[3] = a_DEBUG;
93 - $[4] = t0;
89 + $[0] = props;
90 + $[1] = a_DEBUG;
91 + $[2] = t0;
92 } else {
95 - a_DEBUG = $[3];
96 - t0 = $[4];
93 + a_DEBUG = $[1];
94 + t0 = $[2];
95 }
96 if (t0 !== Symbol.for("react.early_return_sentinel")) {
97 return t0;
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-in-scope.expect.md renamed
+17 -3
@@ -19,6 +19,10 @@ export const FIXTURE_ENTRYPOINT = {
19 sequentialRenders: [
20 { obj: null, objIsNull: true },
21 { obj: { a: 2 }, objIsNull: false },
22 + // check we preserve nullthrows
23 + { obj: { a: undefined }, objIsNull: false },
24 + { obj: undefined, objIsNull: false },
25 + { obj: { a: undefined }, objIsNull: false },
26 ],
27 };
28
@@ -32,7 +36,7 @@ function useFoo(t0) {
36 const $ = useMemoCache(3);
37 const { obj, objIsNull } = t0;
38 let x;
35 - if ($[0] !== objIsNull || $[1] !== obj.a) {
39 + if ($[0] !== objIsNull || $[1] !== obj) {
40 x = [];
41 bb1: {
42 if (objIsNull) {
@@ -42,7 +46,7 @@ function useFoo(t0) {
46 x.push(obj.a);
47 }
48 $[0] = objIsNull;
45 - $[1] = obj.a;
49 + $[1] = obj;
50 $[2] = x;
51 } else {
52 x = $[2];
@@ -56,8 +60,18 @@ export const FIXTURE_ENTRYPOINT = {
60 sequentialRenders: [
61 { obj: null, objIsNull: true },
62 { obj: { a: 2 }, objIsNull: false },
63 + // check we preserve nullthrows
64 + { obj: { a: undefined }, objIsNull: false },
65 + { obj: undefined, objIsNull: false },
66 + { obj: { a: undefined }, objIsNull: false },
67 ],
68 };
69
70 ```
63 -
\ No newline at end of file
71 +
72 +### Eval output
73 +(kind: ok) []
74 +[2]
75 +[null]
76 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]]
77 +[null]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-in-scope.ts renamed
+4
@@ -15,5 +15,9 @@ export const FIXTURE_ENTRYPOINT = {
15 sequentialRenders: [
16 { obj: null, objIsNull: true },
17 { obj: { a: 2 }, objIsNull: false },
18 + // check we preserve nullthrows
19 + { obj: { a: undefined }, objIsNull: false },
20 + { obj: undefined, objIsNull: false },
21 + { obj: { a: undefined }, objIsNull: false },
22 ],
23 };
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-poisons-outer-scope.expect.md new
+94
@@ -0,0 +1,94 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +function useFoo({ input, cond }) {
8 + const x = [];
9 + label: {
10 + if (cond) {
11 + break label;
12 + }
13 + x.push(identity(input.a.b));
14 + }
15 + return x;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useFoo,
20 + params: [{ input: { a: { b: 2 } }, cond: false }],
21 + sequentialRenders: [
22 + { input: { a: { b: 2 } }, cond: false },
23 + // preserve nullthrows
24 + { input: null, cond: false },
25 + { input: null, cond: true },
26 + { input: {}, cond: false },
27 + { input: { a: { b: null } }, cond: false },
28 + { input: { a: null }, cond: false },
29 + { input: { a: { b: 3 } }, cond: false },
30 + ],
31 +};
32 +
33 +```
34 +
35 +## Code
36 +
37 +```javascript
38 +import { unstable_useMemoCache as useMemoCache } from "react";
39 +import { identity } from "shared-runtime";
40 +
41 +function useFoo(t0) {
42 + const $ = useMemoCache(5);
43 + const { input, cond } = t0;
44 + let x;
45 + if ($[0] !== cond || $[1] !== input) {
46 + x = [];
47 + bb1: {
48 + if (cond) {
49 + break bb1;
50 + }
51 + let t1;
52 + if ($[3] !== input.a.b) {
53 + t1 = identity(input.a.b);
54 + $[3] = input.a.b;
55 + $[4] = t1;
56 + } else {
57 + t1 = $[4];
58 + }
59 + x.push(t1);
60 + }
61 + $[0] = cond;
62 + $[1] = input;
63 + $[2] = x;
64 + } else {
65 + x = $[2];
66 + }
67 + return x;
68 +}
69 +
70 +export const FIXTURE_ENTRYPOINT = {
71 + fn: useFoo,
72 + params: [{ input: { a: { b: 2 } }, cond: false }],
73 + sequentialRenders: [
74 + { input: { a: { b: 2 } }, cond: false },
75 + // preserve nullthrows
76 + { input: null, cond: false },
77 + { input: null, cond: true },
78 + { input: {}, cond: false },
79 + { input: { a: { b: null } }, cond: false },
80 + { input: { a: null }, cond: false },
81 + { input: { a: { b: 3 } }, cond: false },
82 + ],
83 +};
84 +
85 +```
86 +
87 +### Eval output
88 +(kind: ok) [2]
89 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]]
90 +[]
91 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]]
92 +[null]
93 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
94 +[3]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-poisons-outer-scope.ts new
+27
@@ -0,0 +1,27 @@
1 +import { identity } from "shared-runtime";
2 +
3 +function useFoo({ input, cond }) {
4 + const x = [];
5 + label: {
6 + if (cond) {
7 + break label;
8 + }
9 + x.push(identity(input.a.b));
10 + }
11 + return x;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: useFoo,
16 + params: [{ input: { a: { b: 2 } }, cond: false }],
17 + sequentialRenders: [
18 + { input: { a: { b: 2 } }, cond: false },
19 + // preserve nullthrows
20 + { input: null, cond: false },
21 + { input: null, cond: true },
22 + { input: {}, cond: false },
23 + { input: { a: { b: null } }, cond: false },
24 + { input: { a: null }, cond: false },
25 + { input: { a: { b: 3 } }, cond: false },
26 + ],
27 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/loop-break-in-scope.expect.md new
+77
@@ -0,0 +1,77 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function useFoo({ obj, objIsNull }) {
6 + const x = [];
7 + for (let i = 0; i < 5; i++) {
8 + if (objIsNull) {
9 + continue;
10 + }
11 + x.push(obj.a);
12 + }
13 + return x;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{ obj: null, objIsNull: true }],
19 + sequentialRenders: [
20 + { obj: null, objIsNull: true },
21 + { obj: { a: 2 }, objIsNull: false },
22 + // check we preserve nullthrows
23 + { obj: { a: undefined }, objIsNull: false },
24 + { obj: undefined, objIsNull: false },
25 + { obj: { a: undefined }, objIsNull: false },
26 + ],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { unstable_useMemoCache as useMemoCache } from "react";
35 +function useFoo(t0) {
36 + const $ = useMemoCache(3);
37 + const { obj, objIsNull } = t0;
38 + let x;
39 + if ($[0] !== objIsNull || $[1] !== obj) {
40 + x = [];
41 + for (let i = 0; i < 5; i++) {
42 + if (objIsNull) {
43 + continue;
44 + }
45 +
46 + x.push(obj.a);
47 + }
48 + $[0] = objIsNull;
49 + $[1] = obj;
50 + $[2] = x;
51 + } else {
52 + x = $[2];
53 + }
54 + return x;
55 +}
56 +
57 +export const FIXTURE_ENTRYPOINT = {
58 + fn: useFoo,
59 + params: [{ obj: null, objIsNull: true }],
60 + sequentialRenders: [
61 + { obj: null, objIsNull: true },
62 + { obj: { a: 2 }, objIsNull: false },
63 + // check we preserve nullthrows
64 + { obj: { a: undefined }, objIsNull: false },
65 + { obj: undefined, objIsNull: false },
66 + { obj: { a: undefined }, objIsNull: false },
67 + ],
68 +};
69 +
70 +```
71 +
72 +### Eval output
73 +(kind: ok) []
74 +[2,2,2,2,2]
75 +[null,null,null,null,null]
76 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]]
77 +[null,null,null,null,null]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/loop-break-in-scope.ts new
+23
@@ -0,0 +1,23 @@
1 +function useFoo({ obj, objIsNull }) {
2 + const x = [];
3 + for (let i = 0; i < 5; i++) {
4 + if (objIsNull) {
5 + continue;
6 + }
7 + x.push(obj.a);
8 + }
9 + return x;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: useFoo,
14 + params: [{ obj: null, objIsNull: true }],
15 + sequentialRenders: [
16 + { obj: null, objIsNull: true },
17 + { obj: { a: 2 }, objIsNull: false },
18 + // check we preserve nullthrows
19 + { obj: { a: undefined }, objIsNull: false },
20 + { obj: undefined, objIsNull: false },
21 + { obj: { a: undefined }, objIsNull: false },
22 + ],
23 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps.expect.md new
+114
@@ -0,0 +1,114 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +function useFoo({ input, cond, hasAB }) {
8 + const x = [];
9 + if (cond) {
10 + if (!hasAB) {
11 + return null;
12 + }
13 + x.push(identity(input.a.b));
14 + } else {
15 + x.push(identity(input.a.b));
16 + }
17 + return x;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [{ input: { b: 1 }, cond: true, hasAB: false }],
23 + sequentialRenders: [
24 + { input: { a: { b: 1 } }, cond: true, hasAB: true },
25 + { input: null, cond: true, hasAB: false },
26 + // preserve nullthrows
27 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
28 + { input: { a: undefined }, cond: true, hasAB: true },
29 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
30 + { input: undefined, cond: true, hasAB: true },
31 + ],
32 +};
33 +
34 +```
35 +
36 +## Code
37 +
38 +```javascript
39 +import { unstable_useMemoCache as useMemoCache } from "react";
40 +import { identity } from "shared-runtime";
41 +
42 +function useFoo(t0) {
43 + const $ = useMemoCache(9);
44 + const { input, cond, hasAB } = t0;
45 + let x;
46 + let t1;
47 + if ($[0] !== cond || $[1] !== hasAB || $[2] !== input) {
48 + t1 = Symbol.for("react.early_return_sentinel");
49 + bb10: {
50 + x = [];
51 + if (cond) {
52 + if (!hasAB) {
53 + t1 = null;
54 + break bb10;
55 + }
56 + let t2;
57 + if ($[5] !== input.a.b) {
58 + t2 = identity(input.a.b);
59 + $[5] = input.a.b;
60 + $[6] = t2;
61 + } else {
62 + t2 = $[6];
63 + }
64 + x.push(t2);
65 + } else {
66 + let t2;
67 + if ($[7] !== input.a.b) {
68 + t2 = identity(input.a.b);
69 + $[7] = input.a.b;
70 + $[8] = t2;
71 + } else {
72 + t2 = $[8];
73 + }
74 + x.push(t2);
75 + }
76 + }
77 + $[0] = cond;
78 + $[1] = hasAB;
79 + $[2] = input;
80 + $[3] = x;
81 + $[4] = t1;
82 + } else {
83 + x = $[3];
84 + t1 = $[4];
85 + }
86 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
87 + return t1;
88 + }
89 + return x;
90 +}
91 +
92 +export const FIXTURE_ENTRYPOINT = {
93 + fn: useFoo,
94 + params: [{ input: { b: 1 }, cond: true, hasAB: false }],
95 + sequentialRenders: [
96 + { input: { a: { b: 1 } }, cond: true, hasAB: true },
97 + { input: null, cond: true, hasAB: false },
98 + // preserve nullthrows
99 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
100 + { input: { a: undefined }, cond: true, hasAB: true },
101 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
102 + { input: undefined, cond: true, hasAB: true },
103 + ],
104 +};
105 +
106 +```
107 +
108 +### Eval output
109 +(kind: ok) [1]
110 +null
111 +[null]
112 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]]
113 +[null]
114 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps.ts new
+28
@@ -0,0 +1,28 @@
1 +import { identity } from "shared-runtime";
2 +
3 +function useFoo({ input, cond, hasAB }) {
4 + const x = [];
5 + if (cond) {
6 + if (!hasAB) {
7 + return null;
8 + }
9 + x.push(identity(input.a.b));
10 + } else {
11 + x.push(identity(input.a.b));
12 + }
13 + return x;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{ input: { b: 1 }, cond: true, hasAB: false }],
19 + sequentialRenders: [
20 + { input: { a: { b: 1 } }, cond: true, hasAB: true },
21 + { input: null, cond: true, hasAB: false },
22 + // preserve nullthrows
23 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
24 + { input: { a: undefined }, cond: true, hasAB: true },
25 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
26 + { input: undefined, cond: true, hasAB: true },
27 + ],
28 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps1.expect.md new
+123
@@ -0,0 +1,123 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +function useFoo({ input, cond, hasAB }) {
8 + const x = [];
9 + if (cond) {
10 + if (!hasAB) {
11 + return null;
12 + } else {
13 + x.push(identity(input.a.b));
14 + }
15 + x.push(identity(input.a.b));
16 + } else {
17 + x.push(identity(input.a.b));
18 + }
19 + return x;
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: useFoo,
24 + params: [{ input: { b: 1 }, cond: true, hasAB: false }],
25 + sequentialRenders: [
26 + { input: { a: { b: 1 } }, cond: true, hasAB: true },
27 + { input: null, cond: true, hasAB: false },
28 + // preserve nullthrows
29 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
30 + { input: { a: null }, cond: true, hasAB: true },
31 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
32 + ],
33 +};
34 +
35 +```
36 +
37 +## Code
38 +
39 +```javascript
40 +import { unstable_useMemoCache as useMemoCache } from "react";
41 +import { identity } from "shared-runtime";
42 +
43 +function useFoo(t0) {
44 + const $ = useMemoCache(11);
45 + const { input, cond, hasAB } = t0;
46 + let x;
47 + let t1;
48 + if ($[0] !== cond || $[1] !== hasAB || $[2] !== input) {
49 + t1 = Symbol.for("react.early_return_sentinel");
50 + bb11: {
51 + x = [];
52 + if (cond) {
53 + if (!hasAB) {
54 + t1 = null;
55 + break bb11;
56 + } else {
57 + let t2;
58 + if ($[5] !== input.a.b) {
59 + t2 = identity(input.a.b);
60 + $[5] = input.a.b;
61 + $[6] = t2;
62 + } else {
63 + t2 = $[6];
64 + }
65 + x.push(t2);
66 + }
67 + let t2;
68 + if ($[7] !== input.a.b) {
69 + t2 = identity(input.a.b);
70 + $[7] = input.a.b;
71 + $[8] = t2;
72 + } else {
73 + t2 = $[8];
74 + }
75 + x.push(t2);
76 + } else {
77 + let t2;
78 + if ($[9] !== input.a.b) {
79 + t2 = identity(input.a.b);
80 + $[9] = input.a.b;
81 + $[10] = t2;
82 + } else {
83 + t2 = $[10];
84 + }
85 + x.push(t2);
86 + }
87 + }
88 + $[0] = cond;
89 + $[1] = hasAB;
90 + $[2] = input;
91 + $[3] = x;
92 + $[4] = t1;
93 + } else {
94 + x = $[3];
95 + t1 = $[4];
96 + }
97 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
98 + return t1;
99 + }
100 + return x;
101 +}
102 +
103 +export const FIXTURE_ENTRYPOINT = {
104 + fn: useFoo,
105 + params: [{ input: { b: 1 }, cond: true, hasAB: false }],
106 + sequentialRenders: [
107 + { input: { a: { b: 1 } }, cond: true, hasAB: true },
108 + { input: null, cond: true, hasAB: false },
109 + // preserve nullthrows
110 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
111 + { input: { a: null }, cond: true, hasAB: true },
112 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
113 + ],
114 +};
115 +
116 +```
117 +
118 +### Eval output
119 +(kind: ok) [1,1]
120 +null
121 +[null,null]
122 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
123 +[null,null]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps1.ts new
+29
@@ -0,0 +1,29 @@
1 +import { identity } from "shared-runtime";
2 +
3 +function useFoo({ input, cond, hasAB }) {
4 + const x = [];
5 + if (cond) {
6 + if (!hasAB) {
7 + return null;
8 + } else {
9 + x.push(identity(input.a.b));
10 + }
11 + x.push(identity(input.a.b));
12 + } else {
13 + x.push(identity(input.a.b));
14 + }
15 + return x;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useFoo,
20 + params: [{ input: { b: 1 }, cond: true, hasAB: false }],
21 + sequentialRenders: [
22 + { input: { a: { b: 1 } }, cond: true, hasAB: true },
23 + { input: null, cond: true, hasAB: false },
24 + // preserve nullthrows
25 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
26 + { input: { a: null }, cond: true, hasAB: true },
27 + { input: { a: { b: undefined } }, cond: true, hasAB: true },
28 + ],
29 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-in-scope.expect.md renamed
+17 -3
@@ -17,6 +17,10 @@ export const FIXTURE_ENTRYPOINT = {
17 sequentialRenders: [
18 { obj: null, objIsNull: true },
19 { obj: { a: 2 }, objIsNull: false },
20 + // check we preserve nullthrows
21 + { obj: { a: undefined }, objIsNull: false },
22 + { obj: undefined, objIsNull: false },
23 + { obj: { a: undefined }, objIsNull: false },
24 ],
25 };
26
@@ -31,7 +35,7 @@ function useFoo(t0) {
35 const { obj, objIsNull } = t0;
36 let x;
37 let t1;
34 - if ($[0] !== objIsNull || $[1] !== obj.b) {
38 + if ($[0] !== objIsNull || $[1] !== obj) {
39 t1 = Symbol.for("react.early_return_sentinel");
40 bb7: {
41 x = [];
@@ -43,7 +47,7 @@ function useFoo(t0) {
47 x.push(obj.b);
48 }
49 $[0] = objIsNull;
46 - $[1] = obj.b;
50 + $[1] = obj;
51 $[2] = x;
52 $[3] = t1;
53 } else {
@@ -62,8 +66,18 @@ export const FIXTURE_ENTRYPOINT = {
66 sequentialRenders: [
67 { obj: null, objIsNull: true },
68 { obj: { a: 2 }, objIsNull: false },
69 + // check we preserve nullthrows
70 + { obj: { a: undefined }, objIsNull: false },
71 + { obj: undefined, objIsNull: false },
72 + { obj: { a: undefined }, objIsNull: false },
73 ],
74 };
75
76 ```
69 -
\ No newline at end of file
77 +
78 +### Eval output
79 +(kind: ok)
80 +[null]
81 +[null]
82 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]]
83 +[null]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-in-scope.ts renamed
+4
@@ -13,5 +13,9 @@ export const FIXTURE_ENTRYPOINT = {
13 sequentialRenders: [
14 { obj: null, objIsNull: true },
15 { obj: { a: 2 }, objIsNull: false },
16 + // check we preserve nullthrows
17 + { obj: { a: undefined }, objIsNull: false },
18 + { obj: undefined, objIsNull: false },
19 + { obj: { a: undefined }, objIsNull: false },
20 ],
21 };
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-poisons-outer-scope.expect.md new
+100
@@ -0,0 +1,100 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +function useFoo({ input, cond }) {
8 + const x = [];
9 + if (cond) {
10 + return null;
11 + }
12 + x.push(identity(input.a.b));
13 + return x;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{ input: { a: { b: 2 } }, cond: false }],
19 + sequentialRenders: [
20 + { input: { a: { b: 2 } }, cond: false },
21 + // preserve nullthrows
22 + { input: null, cond: false },
23 + { input: null, cond: true },
24 + { input: {}, cond: false },
25 + { input: { a: { b: null } }, cond: false },
26 + { input: { a: null }, cond: false },
27 + { input: { a: { b: 3 } }, cond: false },
28 + ],
29 +};
30 +
31 +```
32 +
33 +## Code
34 +
35 +```javascript
36 +import { unstable_useMemoCache as useMemoCache } from "react";
37 +import { identity } from "shared-runtime";
38 +
39 +function useFoo(t0) {
40 + const $ = useMemoCache(6);
41 + const { input, cond } = t0;
42 + let x;
43 + let t1;
44 + if ($[0] !== cond || $[1] !== input) {
45 + t1 = Symbol.for("react.early_return_sentinel");
46 + bb7: {
47 + x = [];
48 + if (cond) {
49 + t1 = null;
50 + break bb7;
51 + }
52 + let t2;
53 + if ($[4] !== input.a.b) {
54 + t2 = identity(input.a.b);
55 + $[4] = input.a.b;
56 + $[5] = t2;
57 + } else {
58 + t2 = $[5];
59 + }
60 + x.push(t2);
61 + }
62 + $[0] = cond;
63 + $[1] = input;
64 + $[2] = x;
65 + $[3] = t1;
66 + } else {
67 + x = $[2];
68 + t1 = $[3];
69 + }
70 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
71 + return t1;
72 + }
73 + return x;
74 +}
75 +
76 +export const FIXTURE_ENTRYPOINT = {
77 + fn: useFoo,
78 + params: [{ input: { a: { b: 2 } }, cond: false }],
79 + sequentialRenders: [
80 + { input: { a: { b: 2 } }, cond: false },
81 + // preserve nullthrows
82 + { input: null, cond: false },
83 + { input: null, cond: true },
84 + { input: {}, cond: false },
85 + { input: { a: { b: null } }, cond: false },
86 + { input: { a: null }, cond: false },
87 + { input: { a: { b: 3 } }, cond: false },
88 + ],
89 +};
90 +
91 +```
92 +
93 +### Eval output
94 +(kind: ok) [2]
95 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]]
96 +null
97 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]]
98 +[null]
99 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
100 +[3]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-poisons-outer-scope.ts new
+25
@@ -0,0 +1,25 @@
1 +import { identity } from "shared-runtime";
2 +
3 +function useFoo({ input, cond }) {
4 + const x = [];
5 + if (cond) {
6 + return null;
7 + }
8 + x.push(identity(input.a.b));
9 + return x;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: useFoo,
14 + params: [{ input: { a: { b: 2 } }, cond: false }],
15 + sequentialRenders: [
16 + { input: { a: { b: 2 } }, cond: false },
17 + // preserve nullthrows
18 + { input: null, cond: false },
19 + { input: null, cond: true },
20 + { input: {}, cond: false },
21 + { input: { a: { b: null } }, cond: false },
22 + { input: { a: null }, cond: false },
23 + { input: { a: { b: 3 } }, cond: false },
24 + ],
25 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/else-branch-scope-unpoisoned.expect.md new
+94
@@ -0,0 +1,94 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +function useFoo({ input, cond }) {
8 + const x = [];
9 + label: {
10 + if (cond) {
11 + break label;
12 + } else {
13 + x.push(identity(input.a.b));
14 + }
15 + }
16 + return x[0];
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: useFoo,
21 + params: [{ input: { a: { b: 2 } }, cond: false }],
22 + sequentialRenders: [
23 + { input: null, cond: true },
24 + { input: { a: { b: 2 } }, cond: false },
25 + { input: null, cond: true },
26 + // preserve nullthrows
27 + { input: {}, cond: false },
28 + { input: { a: { b: null } }, cond: false },
29 + { input: { a: null }, cond: false },
30 + { input: { a: { b: 3 } }, cond: false },
31 + ],
32 +};
33 +
34 +```
35 +
36 +## Code
37 +
38 +```javascript
39 +import { unstable_useMemoCache as useMemoCache } from "react";
40 +import { identity } from "shared-runtime";
41 +
42 +function useFoo(t0) {
43 + const $ = useMemoCache(5);
44 + const { input, cond } = t0;
45 + let x;
46 + if ($[0] !== cond || $[1] !== input) {
47 + x = [];
48 + bb1: if (cond) {
49 + break bb1;
50 + } else {
51 + let t1;
52 + if ($[3] !== input.a.b) {
53 + t1 = identity(input.a.b);
54 + $[3] = input.a.b;
55 + $[4] = t1;
56 + } else {
57 + t1 = $[4];
58 + }
59 + x.push(t1);
60 + }
61 + $[0] = cond;
62 + $[1] = input;
63 + $[2] = x;
64 + } else {
65 + x = $[2];
66 + }
67 + return x[0];
68 +}
69 +
70 +export const FIXTURE_ENTRYPOINT = {
71 + fn: useFoo,
72 + params: [{ input: { a: { b: 2 } }, cond: false }],
73 + sequentialRenders: [
74 + { input: null, cond: true },
75 + { input: { a: { b: 2 } }, cond: false },
76 + { input: null, cond: true },
77 + // preserve nullthrows
78 + { input: {}, cond: false },
79 + { input: { a: { b: null } }, cond: false },
80 + { input: { a: null }, cond: false },
81 + { input: { a: { b: 3 } }, cond: false },
82 + ],
83 +};
84 +
85 +```
86 +
87 +### Eval output
88 +(kind: ok)
89 +2
90 +
91 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]]
92 +null
93 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
94 +3
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/else-branch-scope-unpoisoned.ts new
+28
@@ -0,0 +1,28 @@
1 +import { identity } from "shared-runtime";
2 +
3 +function useFoo({ input, cond }) {
4 + const x = [];
5 + label: {
6 + if (cond) {
7 + break label;
8 + } else {
9 + x.push(identity(input.a.b));
10 + }
11 + }
12 + return x[0];
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: useFoo,
17 + params: [{ input: { a: { b: 2 } }, cond: false }],
18 + sequentialRenders: [
19 + { input: null, cond: true },
20 + { input: { a: { b: 2 } }, cond: false },
21 + { input: null, cond: true },
22 + // preserve nullthrows
23 + { input: {}, cond: false },
24 + { input: { a: { b: null } }, cond: false },
25 + { input: { a: null }, cond: false },
26 + { input: { a: { b: 3 } }, cond: false },
27 + ],
28 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-label.expect.md new
+81
@@ -0,0 +1,81 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function useFoo({ input, cond }) {
6 + const x = [];
7 + label: {
8 + if (cond) {
9 + break label;
10 + }
11 + }
12 + x.push(input.a.b); // unconditional
13 + return x;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{ input: { a: { b: 2 } }, cond: false }],
19 + sequentialRenders: [
20 + { input: { a: { b: 2 } }, cond: false },
21 + // preserve nullthrows
22 + { input: null, cond: false },
23 + { input: null, cond: true },
24 + { input: {}, cond: false },
25 + { input: { a: { b: null } }, cond: false },
26 + { input: { a: null }, cond: false },
27 + { input: { a: { b: 3 } }, cond: false },
28 + ],
29 +};
30 +
31 +```
32 +
33 +## Code
34 +
35 +```javascript
36 +import { unstable_useMemoCache as useMemoCache } from "react";
37 +function useFoo(t0) {
38 + const $ = useMemoCache(3);
39 + const { input, cond } = t0;
40 + let x;
41 + if ($[0] !== cond || $[1] !== input.a.b) {
42 + x = [];
43 + bb1: if (cond) {
44 + break bb1;
45 + }
46 +
47 + x.push(input.a.b);
48 + $[0] = cond;
49 + $[1] = input.a.b;
50 + $[2] = x;
51 + } else {
52 + x = $[2];
53 + }
54 + return x;
55 +}
56 +
57 +export const FIXTURE_ENTRYPOINT = {
58 + fn: useFoo,
59 + params: [{ input: { a: { b: 2 } }, cond: false }],
60 + sequentialRenders: [
61 + { input: { a: { b: 2 } }, cond: false },
62 + // preserve nullthrows
63 + { input: null, cond: false },
64 + { input: null, cond: true },
65 + { input: {}, cond: false },
66 + { input: { a: { b: null } }, cond: false },
67 + { input: { a: null }, cond: false },
68 + { input: { a: { b: 3 } }, cond: false },
69 + ],
70 +};
71 +
72 +```
73 +
74 +### Eval output
75 +(kind: ok) [2]
76 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]]
77 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]]
78 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]]
79 +[null]
80 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
81 +[3]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-label.ts new
+25
@@ -0,0 +1,25 @@
1 +function useFoo({ input, cond }) {
2 + const x = [];
3 + label: {
4 + if (cond) {
5 + break label;
6 + }
7 + }
8 + x.push(input.a.b); // unconditional
9 + return x;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: useFoo,
14 + params: [{ input: { a: { b: 2 } }, cond: false }],
15 + sequentialRenders: [
16 + { input: { a: { b: 2 } }, cond: false },
17 + // preserve nullthrows
18 + { input: null, cond: false },
19 + { input: null, cond: true },
20 + { input: {}, cond: false },
21 + { input: { a: { b: null } }, cond: false },
22 + { input: { a: null }, cond: false },
23 + { input: { a: { b: 3 } }, cond: false },
24 + ],
25 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-loop-break.expect.md new
+86
@@ -0,0 +1,86 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function useFoo({ input, max }) {
6 + const x = [];
7 + let i = 0;
8 + while (true) {
9 + i += 1;
10 + if (i > max) {
11 + break;
12 + }
13 + }
14 + x.push(i);
15 + x.push(input.a.b); // unconditional
16 + return x;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: useFoo,
21 + params: [{ input: { a: { b: 2 } }, max: 8 }],
22 + sequentialRenders: [
23 + { input: { a: { b: 2 } }, max: 8 },
24 + // preserve nullthrows
25 + { input: null, max: 8 },
26 + { input: {}, max: 8 },
27 + { input: { a: { b: null } }, max: 8 },
28 + { input: { a: null }, max: 8 },
29 + { input: { a: { b: 3 } }, max: 8 },
30 + ],
31 +};
32 +
33 +```
34 +
35 +## Code
36 +
37 +```javascript
38 +import { unstable_useMemoCache as useMemoCache } from "react";
39 +function useFoo(t0) {
40 + const $ = useMemoCache(3);
41 + const { input, max } = t0;
42 + let x;
43 + if ($[0] !== max || $[1] !== input.a.b) {
44 + x = [];
45 + let i = 0;
46 + while (true) {
47 + i = i + 1;
48 + if (i > max) {
49 + break;
50 + }
51 + }
52 +
53 + x.push(i);
54 + x.push(input.a.b);
55 + $[0] = max;
56 + $[1] = input.a.b;
57 + $[2] = x;
58 + } else {
59 + x = $[2];
60 + }
61 + return x;
62 +}
63 +
64 +export const FIXTURE_ENTRYPOINT = {
65 + fn: useFoo,
66 + params: [{ input: { a: { b: 2 } }, max: 8 }],
67 + sequentialRenders: [
68 + { input: { a: { b: 2 } }, max: 8 },
69 + // preserve nullthrows
70 + { input: null, max: 8 },
71 + { input: {}, max: 8 },
72 + { input: { a: { b: null } }, max: 8 },
73 + { input: { a: null }, max: 8 },
74 + { input: { a: { b: 3 } }, max: 8 },
75 + ],
76 +};
77 +
78 +```
79 +
80 +### Eval output
81 +(kind: ok) [9,2]
82 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]]
83 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]]
84 +[9,null]
85 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
86 +[9,3]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-loop-break.ts new
+27
@@ -0,0 +1,27 @@
1 +function useFoo({ input, max }) {
2 + const x = [];
3 + let i = 0;
4 + while (true) {
5 + i += 1;
6 + if (i > max) {
7 + break;
8 + }
9 + }
10 + x.push(i);
11 + x.push(input.a.b); // unconditional
12 + return x;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: useFoo,
17 + params: [{ input: { a: { b: 2 } }, max: 8 }],
18 + sequentialRenders: [
19 + { input: { a: { b: 2 } }, max: 8 },
20 + // preserve nullthrows
21 + { input: null, max: 8 },
22 + { input: {}, max: 8 },
23 + { input: { a: { b: null } }, max: 8 },
24 + { input: { a: null }, max: 8 },
25 + { input: { a: { b: 3 } }, max: 8 },
26 + ],
27 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps.expect.md new
+91
@@ -0,0 +1,91 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +function useFoo({ input, hasAB, returnNull }) {
8 + const x = [];
9 + if (!hasAB) {
10 + x.push(identity(input.a));
11 + if (!returnNull) {
12 + return null;
13 + }
14 + } else {
15 + x.push(identity(input.a.b));
16 + }
17 + return x;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [{ input: { b: 1 }, hasAB: false, returnNull: false }],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +import { unstable_useMemoCache as useMemoCache } from "react";
31 +import { identity } from "shared-runtime";
32 +
33 +function useFoo(t0) {
34 + const $ = useMemoCache(9);
35 + const { input, hasAB, returnNull } = t0;
36 + let x;
37 + let t1;
38 + if ($[0] !== hasAB || $[1] !== input.a || $[2] !== returnNull) {
39 + t1 = Symbol.for("react.early_return_sentinel");
40 + bb10: {
41 + x = [];
42 + if (!hasAB) {
43 + let t2;
44 + if ($[5] !== input.a) {
45 + t2 = identity(input.a);
46 + $[5] = input.a;
47 + $[6] = t2;
48 + } else {
49 + t2 = $[6];
50 + }
51 + x.push(t2);
52 + if (!returnNull) {
53 + t1 = null;
54 + break bb10;
55 + }
56 + } else {
57 + let t2;
58 + if ($[7] !== input.a.b) {
59 + t2 = identity(input.a.b);
60 + $[7] = input.a.b;
61 + $[8] = t2;
62 + } else {
63 + t2 = $[8];
64 + }
65 + x.push(t2);
66 + }
67 + }
68 + $[0] = hasAB;
69 + $[1] = input.a;
70 + $[2] = returnNull;
71 + $[3] = x;
72 + $[4] = t1;
73 + } else {
74 + x = $[3];
75 + t1 = $[4];
76 + }
77 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
78 + return t1;
79 + }
80 + return x;
81 +}
82 +
83 +export const FIXTURE_ENTRYPOINT = {
84 + fn: useFoo,
85 + params: [{ input: { b: 1 }, hasAB: false, returnNull: false }],
86 +};
87 +
88 +```
89 +
90 +### Eval output
91 +(kind: ok) null
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps.ts new
+19
@@ -0,0 +1,19 @@
1 +import { identity } from "shared-runtime";
2 +
3 +function useFoo({ input, hasAB, returnNull }) {
4 + const x = [];
5 + if (!hasAB) {
6 + x.push(identity(input.a));
7 + if (!returnNull) {
8 + return null;
9 + }
10 + } else {
11 + x.push(identity(input.a.b));
12 + }
13 + return x;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{ input: { b: 1 }, hasAB: false, returnNull: false }],
19 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps1.expect.md new
+123
@@ -0,0 +1,123 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +function useFoo({ input, cond2, cond1 }) {
8 + const x = [];
9 + if (cond1) {
10 + if (!cond2) {
11 + x.push(identity(input.a.b));
12 + return null;
13 + } else {
14 + x.push(identity(input.a.b));
15 + }
16 + } else {
17 + x.push(identity(input.a.b));
18 + }
19 + return x;
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: useFoo,
24 + params: [{ input: { b: 1 }, cond1: true, cond2: false }],
25 + sequentialRenders: [
26 + { input: { a: { b: 1 } }, cond1: true, cond2: true },
27 + { input: null, cond1: true, cond2: false },
28 + // preserve nullthrows
29 + { input: { a: { b: undefined } }, cond1: true, cond2: true },
30 + { input: { a: null }, cond1: true, cond2: true },
31 + { input: { a: { b: undefined } }, cond1: true, cond2: true },
32 + ],
33 +};
34 +
35 +```
36 +
37 +## Code
38 +
39 +```javascript
40 +import { unstable_useMemoCache as useMemoCache } from "react";
41 +import { identity } from "shared-runtime";
42 +
43 +function useFoo(t0) {
44 + const $ = useMemoCache(11);
45 + const { input, cond2, cond1 } = t0;
46 + let x;
47 + let t1;
48 + if ($[0] !== cond1 || $[1] !== cond2 || $[2] !== input.a.b) {
49 + t1 = Symbol.for("react.early_return_sentinel");
50 + bb11: {
51 + x = [];
52 + if (cond1) {
53 + if (!cond2) {
54 + let t2;
55 + if ($[5] !== input.a.b) {
56 + t2 = identity(input.a.b);
57 + $[5] = input.a.b;
58 + $[6] = t2;
59 + } else {
60 + t2 = $[6];
61 + }
62 + x.push(t2);
63 + t1 = null;
64 + break bb11;
65 + } else {
66 + let t2;
67 + if ($[7] !== input.a.b) {
68 + t2 = identity(input.a.b);
69 + $[7] = input.a.b;
70 + $[8] = t2;
71 + } else {
72 + t2 = $[8];
73 + }
74 + x.push(t2);
75 + }
76 + } else {
77 + let t2;
78 + if ($[9] !== input.a.b) {
79 + t2 = identity(input.a.b);
80 + $[9] = input.a.b;
81 + $[10] = t2;
82 + } else {
83 + t2 = $[10];
84 + }
85 + x.push(t2);
86 + }
87 + }
88 + $[0] = cond1;
89 + $[1] = cond2;
90 + $[2] = input.a.b;
91 + $[3] = x;
92 + $[4] = t1;
93 + } else {
94 + x = $[3];
95 + t1 = $[4];
96 + }
97 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
98 + return t1;
99 + }
100 + return x;
101 +}
102 +
103 +export const FIXTURE_ENTRYPOINT = {
104 + fn: useFoo,
105 + params: [{ input: { b: 1 }, cond1: true, cond2: false }],
106 + sequentialRenders: [
107 + { input: { a: { b: 1 } }, cond1: true, cond2: true },
108 + { input: null, cond1: true, cond2: false },
109 + // preserve nullthrows
110 + { input: { a: { b: undefined } }, cond1: true, cond2: true },
111 + { input: { a: null }, cond1: true, cond2: true },
112 + { input: { a: { b: undefined } }, cond1: true, cond2: true },
113 + ],
114 +};
115 +
116 +```
117 +
118 +### Eval output
119 +(kind: ok) [1]
120 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]]
121 +[null]
122 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
123 +[null]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps1.ts new
+29
@@ -0,0 +1,29 @@
1 +import { identity } from "shared-runtime";
2 +
3 +function useFoo({ input, cond2, cond1 }) {
4 + const x = [];
5 + if (cond1) {
6 + if (!cond2) {
7 + x.push(identity(input.a.b));
8 + return null;
9 + } else {
10 + x.push(identity(input.a.b));
11 + }
12 + } else {
13 + x.push(identity(input.a.b));
14 + }
15 + return x;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useFoo,
20 + params: [{ input: { b: 1 }, cond1: true, cond2: false }],
21 + sequentialRenders: [
22 + { input: { a: { b: 1 } }, cond1: true, cond2: true },
23 + { input: null, cond1: true, cond2: false },
24 + // preserve nullthrows
25 + { input: { a: { b: undefined } }, cond1: true, cond2: true },
26 + { input: { a: null }, cond1: true, cond2: true },
27 + { input: { a: { b: undefined } }, cond1: true, cond2: true },
28 + ],
29 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/return-before-scope-starts.expect.md new
+90
@@ -0,0 +1,90 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { arrayPush } from "shared-runtime";
6 +
7 +function useFoo({ input, cond }) {
8 + if (cond) {
9 + return { result: "early return" };
10 + }
11 +
12 + // unconditional
13 + const x = [];
14 + arrayPush(x, input.a.b);
15 + return x;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useFoo,
20 + params: [{ input: { a: { b: 2 } }, cond: false }],
21 + sequentialRenders: [
22 + { input: null, cond: true },
23 + { input: { a: { b: 2 } }, cond: false },
24 + { input: null, cond: true },
25 + // preserve nullthrows
26 + { input: {}, cond: false },
27 + { input: { a: { b: null } }, cond: false },
28 + { input: { a: null }, cond: false },
29 + { input: { a: { b: 3 } }, cond: false },
30 + ],
31 +};
32 +
33 +```
34 +
35 +## Code
36 +
37 +```javascript
38 +import { unstable_useMemoCache as useMemoCache } from "react";
39 +import { arrayPush } from "shared-runtime";
40 +
41 +function useFoo(t0) {
42 + const $ = useMemoCache(3);
43 + const { input, cond } = t0;
44 + if (cond) {
45 + let t1;
46 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
47 + t1 = { result: "early return" };
48 + $[0] = t1;
49 + } else {
50 + t1 = $[0];
51 + }
52 + return t1;
53 + }
54 + let x;
55 + if ($[1] !== input.a.b) {
56 + x = [];
57 + arrayPush(x, input.a.b);
58 + $[1] = input.a.b;
59 + $[2] = x;
60 + } else {
61 + x = $[2];
62 + }
63 + return x;
64 +}
65 +
66 +export const FIXTURE_ENTRYPOINT = {
67 + fn: useFoo,
68 + params: [{ input: { a: { b: 2 } }, cond: false }],
69 + sequentialRenders: [
70 + { input: null, cond: true },
71 + { input: { a: { b: 2 } }, cond: false },
72 + { input: null, cond: true },
73 + // preserve nullthrows
74 + { input: {}, cond: false },
75 + { input: { a: { b: null } }, cond: false },
76 + { input: { a: null }, cond: false },
77 + { input: { a: { b: 3 } }, cond: false },
78 + ],
79 +};
80 +
81 +```
82 +
83 +### Eval output
84 +(kind: ok) {"result":"early return"}
85 +[2]
86 +{"result":"early return"}
87 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]]
88 +[null]
89 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
90 +[3]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/return-before-scope-starts.ts new
+27
@@ -0,0 +1,27 @@
1 +import { arrayPush } from "shared-runtime";
2 +
3 +function useFoo({ input, cond }) {
4 + if (cond) {
5 + return { result: "early return" };
6 + }
7 +
8 + // unconditional
9 + const x = [];
10 + arrayPush(x, input.a.b);
11 + return x;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: useFoo,
16 + params: [{ input: { a: { b: 2 } }, cond: false }],
17 + sequentialRenders: [
18 + { input: null, cond: true },
19 + { input: { a: { b: 2 } }, cond: false },
20 + { input: null, cond: true },
21 + // preserve nullthrows
22 + { input: {}, cond: false },
23 + { input: { a: { b: null } }, cond: false },
24 + { input: { a: null }, cond: false },
25 + { input: { a: { b: 3 } }, cond: false },
26 + ],
27 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.expect.md new
+83
@@ -0,0 +1,83 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { arrayPush } from "shared-runtime";
6 +
7 +function useFoo({ input, cond }) {
8 + if (cond) {
9 + throw new Error("throw with error!");
10 + }
11 +
12 + // unconditional
13 + const x = [];
14 + arrayPush(x, input.a.b);
15 + return x;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useFoo,
20 + params: [{ input: { a: { b: 2 } }, cond: false }],
21 + sequentialRenders: [
22 + { input: null, cond: true },
23 + { input: { a: { b: 2 } }, cond: false },
24 + { input: null, cond: true },
25 + // preserve nullthrows
26 + { input: {}, cond: false },
27 + { input: { a: { b: null } }, cond: false },
28 + { input: { a: null }, cond: false },
29 + { input: { a: { b: 3 } }, cond: false },
30 + ],
31 +};
32 +
33 +```
34 +
35 +## Code
36 +
37 +```javascript
38 +import { unstable_useMemoCache as useMemoCache } from "react";
39 +import { arrayPush } from "shared-runtime";
40 +
41 +function useFoo(t0) {
42 + const $ = useMemoCache(2);
43 + const { input, cond } = t0;
44 + if (cond) {
45 + throw new Error("throw with error!");
46 + }
47 + let x;
48 + if ($[0] !== input.a.b) {
49 + x = [];
50 + arrayPush(x, input.a.b);
51 + $[0] = input.a.b;
52 + $[1] = x;
53 + } else {
54 + x = $[1];
55 + }
56 + return x;
57 +}
58 +
59 +export const FIXTURE_ENTRYPOINT = {
60 + fn: useFoo,
61 + params: [{ input: { a: { b: 2 } }, cond: false }],
62 + sequentialRenders: [
63 + { input: null, cond: true },
64 + { input: { a: { b: 2 } }, cond: false },
65 + { input: null, cond: true },
66 + // preserve nullthrows
67 + { input: {}, cond: false },
68 + { input: { a: { b: null } }, cond: false },
69 + { input: { a: null }, cond: false },
70 + { input: { a: { b: 3 } }, cond: false },
71 + ],
72 +};
73 +
74 +```
75 +
76 +### Eval output
77 +(kind: ok) [[ (exception in render) Error: throw with error! ]]
78 +[[ (exception in render) Error: throw with error! ]]
79 +[[ (exception in render) Error: throw with error! ]]
80 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]]
81 +[null]
82 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
83 +[3]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.ts new
+27
@@ -0,0 +1,27 @@
1 +import { arrayPush } from "shared-runtime";
2 +
3 +function useFoo({ input, cond }) {
4 + if (cond) {
5 + throw new Error("throw with error!");
6 + }
7 +
8 + // unconditional
9 + const x = [];
10 + arrayPush(x, input.a.b);
11 + return x;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: useFoo,
16 + params: [{ input: { a: { b: 2 } }, cond: false }],
17 + sequentialRenders: [
18 + { input: null, cond: true },
19 + { input: { a: { b: 2 } }, cond: false },
20 + { input: null, cond: true },
21 + // preserve nullthrows
22 + { input: {}, cond: false },
23 + { input: { a: { b: null } }, cond: false },
24 + { input: { a: null }, cond: false },
25 + { input: { a: { b: 3 } }, cond: false },
26 + ],
27 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/reduce-if-exhaustive-poisoned-deps.expect.md new
+101
@@ -0,0 +1,101 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +function useFoo({ input, inputHasAB, inputHasABC }) {
8 + const x = [];
9 + if (!inputHasABC) {
10 + x.push(identity(input.a));
11 + if (!inputHasAB) {
12 + return null;
13 + }
14 + x.push(identity(input.a.b));
15 + } else {
16 + x.push(identity(input.a.b.c));
17 + }
18 + return x;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: useFoo,
23 + params: [{ input: { b: 1 }, inputHasAB: false, inputHasABC: false }],
24 +};
25 +
26 +```
27 +
28 +## Code
29 +
30 +```javascript
31 +import { unstable_useMemoCache as useMemoCache } from "react";
32 +import { identity } from "shared-runtime";
33 +
34 +function useFoo(t0) {
35 + const $ = useMemoCache(11);
36 + const { input, inputHasAB, inputHasABC } = t0;
37 + let x;
38 + let t1;
39 + if ($[0] !== inputHasABC || $[1] !== input.a || $[2] !== inputHasAB) {
40 + t1 = Symbol.for("react.early_return_sentinel");
41 + bb10: {
42 + x = [];
43 + if (!inputHasABC) {
44 + let t2;
45 + if ($[5] !== input.a) {
46 + t2 = identity(input.a);
47 + $[5] = input.a;
48 + $[6] = t2;
49 + } else {
50 + t2 = $[6];
51 + }
52 + x.push(t2);
53 + if (!inputHasAB) {
54 + t1 = null;
55 + break bb10;
56 + }
57 + let t3;
58 + if ($[7] !== input.a.b) {
59 + t3 = identity(input.a.b);
60 + $[7] = input.a.b;
61 + $[8] = t3;
62 + } else {
63 + t3 = $[8];
64 + }
65 + x.push(t3);
66 + } else {
67 + let t2;
68 + if ($[9] !== input.a.b.c) {
69 + t2 = identity(input.a.b.c);
70 + $[9] = input.a.b.c;
71 + $[10] = t2;
72 + } else {
73 + t2 = $[10];
74 + }
75 + x.push(t2);
76 + }
77 + }
78 + $[0] = inputHasABC;
79 + $[1] = input.a;
80 + $[2] = inputHasAB;
81 + $[3] = x;
82 + $[4] = t1;
83 + } else {
84 + x = $[3];
85 + t1 = $[4];
86 + }
87 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
88 + return t1;
89 + }
90 + return x;
91 +}
92 +
93 +export const FIXTURE_ENTRYPOINT = {
94 + fn: useFoo,
95 + params: [{ input: { b: 1 }, inputHasAB: false, inputHasABC: false }],
96 +};
97 +
98 +```
99 +
100 +### Eval output
101 +(kind: ok) null
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/reduce-if-exhaustive-poisoned-deps.ts new
+20
@@ -0,0 +1,20 @@
1 +import { identity } from "shared-runtime";
2 +
3 +function useFoo({ input, inputHasAB, inputHasABC }) {
4 + const x = [];
5 + if (!inputHasABC) {
6 + x.push(identity(input.a));
7 + if (!inputHasAB) {
8 + return null;
9 + }
10 + x.push(identity(input.a.b));
11 + } else {
12 + x.push(identity(input.a.b.c));
13 + }
14 + return x;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: useFoo,
19 + params: [{ input: { b: 1 }, inputHasAB: false, inputHasABC: false }],
20 +};