From 51668692046ebd997d93c45a91f7cc1ca1479fac Mon Sep 17 00:00:00 2001 From: Mofei Zhang Date: Wed, 27 Mar 2024 20:26:18 -0400 Subject: [PATCH] [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. image 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. image --- 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`. image --- --- .../PropagateScopeDependencies.ts | 282 +++++++++++++++++- .../src/Utils/Stack.ts | 18 ++ .../conditional-break-labeled.expect.md | 18 +- .../conditional-early-return.expect.md | 16 +- .../jump-poisoned/break-in-scope.expect.md} | 20 +- .../jump-poisoned/break-in-scope.ts} | 4 + .../break-poisons-outer-scope.expect.md | 94 ++++++ .../break-poisons-outer-scope.ts | 27 ++ .../loop-break-in-scope.expect.md | 77 +++++ .../jump-poisoned/loop-break-in-scope.ts | 23 ++ ...e-if-nonexhaustive-poisoned-deps.expect.md | 114 +++++++ .../reduce-if-nonexhaustive-poisoned-deps.ts | 28 ++ ...-if-nonexhaustive-poisoned-deps1.expect.md | 123 ++++++++ .../reduce-if-nonexhaustive-poisoned-deps1.ts | 29 ++ .../jump-poisoned/return-in-scope.expect.md} | 20 +- .../jump-poisoned/return-in-scope.ts} | 4 + .../return-poisons-outer-scope.expect.md | 100 +++++++ .../return-poisons-outer-scope.ts | 25 ++ .../else-branch-scope-unpoisoned.expect.md | 94 ++++++ .../else-branch-scope-unpoisoned.ts | 28 ++ .../jump-target-within-scope-label.expect.md | 81 +++++ .../jump-target-within-scope-label.ts | 25 ++ ...p-target-within-scope-loop-break.expect.md | 86 ++++++ .../jump-target-within-scope-loop-break.ts | 27 ++ ...e-if-exhaustive-nonpoisoned-deps.expect.md | 91 ++++++ .../reduce-if-exhaustive-nonpoisoned-deps.ts | 19 ++ ...-if-exhaustive-nonpoisoned-deps1.expect.md | 123 ++++++++ .../reduce-if-exhaustive-nonpoisoned-deps1.ts | 29 ++ .../return-before-scope-starts.expect.md | 90 ++++++ .../return-before-scope-starts.ts | 27 ++ .../throw-before-scope-starts.expect.md | 83 ++++++ .../throw-before-scope-starts.ts | 27 ++ ...duce-if-exhaustive-poisoned-deps.expect.md | 101 +++++++ .../reduce-if-exhaustive-poisoned-deps.ts | 20 ++ 34 files changed, 1929 insertions(+), 44 deletions(-) rename compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/{bug-reduce-reactive-deps-break-in-scope.expect.md => reduce-reactive-deps/jump-poisoned/break-in-scope.expect.md} (62%) rename compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/{bug-reduce-reactive-deps-break-in-scope.ts => reduce-reactive-deps/jump-poisoned/break-in-scope.ts} (66%) create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-poisons-outer-scope.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-poisons-outer-scope.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/loop-break-in-scope.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/loop-break-in-scope.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps1.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps1.ts rename compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/{bug-reduce-reactive-deps-return-in-scope.expect.md => reduce-reactive-deps/jump-poisoned/return-in-scope.expect.md} (66%) rename compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/{bug-reduce-reactive-deps-return-in-scope.ts => reduce-reactive-deps/jump-poisoned/return-in-scope.ts} (64%) create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-poisons-outer-scope.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-poisons-outer-scope.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/else-branch-scope-unpoisoned.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/else-branch-scope-unpoisoned.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-label.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-label.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-loop-break.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-loop-break.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps1.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps1.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/return-before-scope-starts.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/return-before-scope-starts.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.ts create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/reduce-if-exhaustive-poisoned-deps.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/reduce-if-exhaustive-poisoned-deps.ts diff --git a/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PropagateScopeDependencies.ts b/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PropagateScopeDependencies.ts index 1e814c7cc5..546967d4dd 100644 --- a/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PropagateScopeDependencies.ts +++ b/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PropagateScopeDependencies.ts @@ -7,6 +7,8 @@ import { CompilerError } from "../CompilerError"; import { + BlockId, + GeneratedSource, Identifier, IdentifierId, InstructionId, @@ -135,9 +137,152 @@ class FindPromotedTemporaries extends ReactiveFunctionVisitor; type Decl = { id: InstructionId; - scope: Stack; + scope: Stack; }; +/** + * TraversalState and PoisonState is used to track the poisoned state of a scope. + * + * A scope is poisoned when either of these conditions hold: + * - one of its own nested blocks is a jump target (for break/continues) + * - it is a outermost scope and contains a throw / return + * + * When a scope is poisoned, all dependencies (from instructions and inner scopes) + * are added as conditionally accessed. + */ +type ScopeTraversalState = { + value: ReactiveScope; + ownBlocks: Stack; +}; + +class PoisonState { + poisonedBlocks: Set = new Set(); + poisonedScopes: Set = new Set(); + isPoisoned: boolean = false; + + constructor( + poisonedBlocks: Set, + poisonedScopes: Set, + isPoisoned: boolean + ) { + this.poisonedBlocks = poisonedBlocks; + this.poisonedScopes = poisonedScopes; + this.isPoisoned = isPoisoned; + } + + clone(): PoisonState { + return new PoisonState( + new Set(this.poisonedBlocks), + new Set(this.poisonedScopes), + this.isPoisoned + ); + } + + take(other: PoisonState): PoisonState { + const copy = new PoisonState( + this.poisonedBlocks, + this.poisonedScopes, + this.isPoisoned + ); + this.poisonedBlocks = other.poisonedBlocks; + this.poisonedScopes = other.poisonedScopes; + this.isPoisoned = other.isPoisoned; + return copy; + } + + merge( + others: Array, + currentScope: ScopeTraversalState | null + ): void { + for (const other of others) { + for (const id of other.poisonedBlocks) { + this.poisonedBlocks.add(id); + } + for (const id of other.poisonedScopes) { + this.poisonedScopes.add(id); + } + } + this.#invalidate(currentScope); + } + + #invalidate(currentScope: ScopeTraversalState | null): void { + if (currentScope != null) { + if (this.poisonedScopes.has(currentScope.value.id)) { + this.isPoisoned = true; + return; + } else if ( + currentScope.ownBlocks.find((blockId) => + this.poisonedBlocks.has(blockId) + ) + ) { + this.isPoisoned = true; + return; + } + } + this.isPoisoned = false; + } + + /** + * Mark a block or scope as poisoned and update the `isPoisoned` flag. + * + * @param targetBlock id of the block which ends non-linear control flow. + * For a break/continue instruction, this is the target block. + * Throw and return instructions have no target and will poison the earliest + * active scope + */ + addPoisonTarget( + target: BlockId | null, + activeScopes: Stack + ): void { + const currentScope = activeScopes.value; + if (target == null && currentScope != null) { + let cursor = activeScopes; + while (true) { + const next = cursor.pop(); + if (next.value == null) { + const poisonedScope = cursor.value!.value.id; + this.poisonedScopes.add(poisonedScope); + if (poisonedScope === currentScope?.value.id) { + this.isPoisoned = true; + } + break; + } else { + cursor = next; + } + } + } else if (target != null) { + this.poisonedBlocks.add(target); + if ( + !this.isPoisoned && + currentScope?.ownBlocks.find((blockId) => blockId === target) + ) { + this.isPoisoned = true; + } + } + } + + /** + * Invoked during traversal when a poisoned scope becomes inactive + * @param id + * @param currentScope + */ + removeMaybePoisonedScope( + id: ScopeId, + currentScope: ScopeTraversalState | null + ): void { + this.poisonedScopes.delete(id); + this.#invalidate(currentScope); + } + + removeMaybePoisonedBlock( + id: BlockId, + currentScope: ScopeTraversalState | null + ): void { + this.poisonedBlocks.delete(id); + this.#invalidate(currentScope); + } +} + class Context { #temporariesUsedOutsideScope: Set; #declarations: DeclMap = new Map(); @@ -163,7 +308,8 @@ class Context { */ #depsInCurrentConditional: ReactiveScopeDependencyTree = new ReactiveScopeDependencyTree(); - #scopes: Stack = empty(); + #scopes: Stack = empty(); + poisonState: PoisonState = new PoisonState(new Set(), new Set(), false); constructor(temporariesUsedOutsideScope: Set) { this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope; @@ -173,6 +319,13 @@ class Context { // Save context of previous scope const prevInConditional = this.#inConditionalWithinScope; const previousDependencies = this.#dependencies; + const prevDepsInConditional: ReactiveScopeDependencyTree | null = this + .isPoisoned + ? this.#depsInCurrentConditional + : null; + if (prevDepsInConditional != null) { + this.#depsInCurrentConditional = new ReactiveScopeDependencyTree(); + } /* * Set context for new scope @@ -183,12 +336,18 @@ class Context { const scopedDependencies = new ReactiveScopeDependencyTree(); this.#inConditionalWithinScope = false; this.#dependencies = scopedDependencies; - this.#scopes = this.#scopes.push(scope); + this.#scopes = this.#scopes.push({ + value: scope, + ownBlocks: empty(), + }); + this.poisonState.isPoisoned = false; fn(); // Restore context of previous scope this.#scopes = this.#scopes.pop(); + this.poisonState.removeMaybePoisonedScope(scope.id, this.#scopes.value); + this.#dependencies = previousDependencies; this.#inConditionalWithinScope = prevInConditional; @@ -204,9 +363,20 @@ class Context { */ this.#dependencies.addDepsFromInnerScope( scopedDependencies, - this.#inConditionalWithinScope, + this.#inConditionalWithinScope || this.isPoisoned, this.#checkValidDependency.bind(this) ); + + if (prevDepsInConditional != null) { + // Outer scope is poisoned + prevDepsInConditional.addDepsFromInnerScope( + this.#depsInCurrentConditional, + true, + this.#checkValidDependency.bind(this) + ); + this.#depsInCurrentConditional = prevDepsInConditional; + } + return minInnerScopeDependencies; } @@ -368,13 +538,13 @@ class Context { const currentDeclaration = this.#reassignments.get(identifier) ?? this.#declarations.get(identifier.id); - const currentScope = this.#scopes !== null ? this.#scopes.value : null; + const currentScope = this.currentScope.value?.value; return ( currentScope != null && currentDeclaration !== undefined && currentDeclaration.id < currentScope.range.start && (currentDeclaration.scope == null || - currentDeclaration.scope.value !== currentScope) + currentDeclaration.scope.value?.value !== currentScope) ); } @@ -382,13 +552,17 @@ class Context { if (this.#scopes === null) { return false; } - return this.#scopes.contains(scope); + return this.#scopes.find((state) => state.value === scope); } - get currentScope(): Stack { + get currentScope(): Stack { return this.#scopes; } + get isPoisoned(): boolean { + return this.poisonState.isPoisoned; + } + visitOperand(place: Place): void { const resolved = this.resolveTemporary(place); /* @@ -436,22 +610,26 @@ class Context { originalDeclaration.scope.value !== null ) { originalDeclaration.scope.each((scope) => { - if (!this.#isScopeActive(scope)) { - scope.declarations.set(maybeDependency.identifier.id, { + if (!this.#isScopeActive(scope.value)) { + scope.value.declarations.set(maybeDependency.identifier.id, { identifier: maybeDependency.identifier, - scope: originalDeclaration.scope.value!, // checked above + scope: originalDeclaration.scope.value!.value, }); } }); } if (this.#checkValidDependency(maybeDependency)) { - this.#depsInCurrentConditional.add(maybeDependency, false); + const isPoisoned = this.isPoisoned; + this.#depsInCurrentConditional.add(maybeDependency, isPoisoned); /* * Add info about this dependency to the existing tree * We do not try to join/reduce dependencies here due to missing info */ - this.#dependencies.add(maybeDependency, this.#inConditionalWithinScope); + this.#dependencies.add( + maybeDependency, + this.#inConditionalWithinScope || isPoisoned + ); } } @@ -460,16 +638,37 @@ class Context { * current one as a {@link ReactiveScope.reassignments} */ visitReassignment(place: Place): void { + const currentScope = this.currentScope.value?.value; if ( - this.currentScope.value != null && - !Array.from(this.currentScope.value.reassignments).some( + currentScope != null && + !Array.from(currentScope.reassignments).some( (identifier) => identifier.id === place.identifier.id ) && this.#checkValidDependency({ identifier: place.identifier, path: [] }) ) { - this.currentScope.value.reassignments.add(place.identifier); + currentScope.reassignments.add(place.identifier); } } + + pushLabeledBlock(id: BlockId): void { + const currentScope = this.#scopes.value; + if (currentScope != null) { + currentScope.ownBlocks = currentScope.ownBlocks.push(id); + } + } + popLabeledBlock(id: BlockId): void { + const currentScope = this.#scopes.value; + if (currentScope != null) { + const last = currentScope.ownBlocks.value; + currentScope.ownBlocks = currentScope.ownBlocks.pop(); + + CompilerError.invariant(last != null && last === id, { + reason: "[PropagateScopeDependencies] Misformed block stack", + loc: GeneratedSource, + }); + } + this.poisonState.removeMaybePoisonedBlock(id, currentScope); + } } class PropagationVisitor extends ReactiveFunctionVisitor { @@ -659,10 +858,38 @@ class PropagationVisitor extends ReactiveFunctionVisitor { } } + enterTerminal(stmt: ReactiveTerminalStatement, context: Context): void { + if (stmt.label != null) { + context.pushLabeledBlock(stmt.label.id); + } + const terminal = stmt.terminal; + switch (terminal.kind) { + case "continue": + case "break": { + context.poisonState.addPoisonTarget( + terminal.target, + context.currentScope + ); + break; + } + case "throw": + case "return": { + context.poisonState.addPoisonTarget(null, context.currentScope); + break; + } + } + } + exitTerminal(stmt: ReactiveTerminalStatement, context: Context): void { + if (stmt.label != null) { + context.popLabeledBlock(stmt.label.id); + } + } + override visitTerminal( stmt: ReactiveTerminalStatement, context: Context ): void { + this.enterTerminal(stmt, context); const terminal = stmt.terminal; switch (terminal.kind) { case "break": @@ -719,13 +946,23 @@ class PropagationVisitor extends ReactiveFunctionVisitor { case "if": { context.visitOperand(terminal.test); const { consequent, alternate } = terminal; + /* + * Consequent and alternate branches are mutually exclusive, + * so we save and restore the poison state here. + */ + const prevPoisonState = context.poisonState.clone(); const depsInIf = context.enterConditional(() => { this.visitBlock(consequent, context); }); if (alternate !== null) { + const ifPoisonState = context.poisonState.take(prevPoisonState); const depsInElse = context.enterConditional(() => { this.visitBlock(alternate, context); }); + context.poisonState.merge( + [ifPoisonState], + context.currentScope.value + ); context.promoteDepsFromExhaustiveConditionals([depsInIf, depsInElse]); } break; @@ -743,6 +980,11 @@ class PropagationVisitor extends ReactiveFunctionVisitor { } const depsInCases = []; let foundDefault = false; + /** + * Switch branches are mutually exclusive + */ + const prevPoisonState = context.poisonState.clone(); + const mutExPoisonStates: Array = []; /* * This can underestimate unconditional accesses due to the current * CFG representation for fallthrough. This is safe. It only @@ -755,6 +997,9 @@ class PropagationVisitor extends ReactiveFunctionVisitor { foundDefault = true; } if (block !== undefined) { + mutExPoisonStates.push( + context.poisonState.take(prevPoisonState.clone()) + ); depsInCases.push( context.enterConditional(() => { this.visitBlock(block, context); @@ -765,6 +1010,10 @@ class PropagationVisitor extends ReactiveFunctionVisitor { if (foundDefault) { context.promoteDepsFromExhaustiveConditionals(depsInCases); } + context.poisonState.merge( + mutExPoisonStates, + context.currentScope.value + ); break; } case "label": { @@ -783,5 +1032,6 @@ class PropagationVisitor extends ReactiveFunctionVisitor { ); } } + this.exitTerminal(stmt, context); } } diff --git a/compiler/packages/babel-plugin-react-forget/src/Utils/Stack.ts b/compiler/packages/babel-plugin-react-forget/src/Utils/Stack.ts index c409e20d6c..f387e907ab 100644 --- a/compiler/packages/babel-plugin-react-forget/src/Utils/Stack.ts +++ b/compiler/packages/babel-plugin-react-forget/src/Utils/Stack.ts @@ -25,10 +25,13 @@ interface StackInterface { pop(): StackInterface; contains(value: T): boolean; + find(fn: (value: T) => boolean): boolean; each(fn: (value: T) => void): void; get value(): T | null; + + print(fn: (node: T) => string): string; } export function create(value: T): Stack { @@ -56,6 +59,10 @@ class Node implements StackInterface { return this.#next; } + find(fn: (value: T) => boolean): boolean { + return fn(this.#value) ? true : this.#next.find(fn); + } + contains(value: T): boolean { return ( value === this.#value || @@ -70,6 +77,10 @@ class Node implements StackInterface { get value(): T { return this.#value; } + + print(fn: (node: T) => string): string { + return fn(this.#value) + this.#next.print(fn); + } } class Empty implements StackInterface { @@ -79,6 +90,10 @@ class Empty implements StackInterface { pop(): Stack { return this; } + + find(_fn: (value: T) => boolean): boolean { + return false; + } contains(_value: T): boolean { return false; } @@ -88,6 +103,9 @@ class Empty implements StackInterface { get value(): T | null { return null; } + print(_: (node: T) => string): string { + return ""; + } } const EMPTY: Stack = new Empty(); diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md index 2f5773825e..4f87e9f80c 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md @@ -34,14 +34,9 @@ import { unstable_useMemoCache as useMemoCache } from "react"; * props.b *does* influence `a` */ function Component(props) { - const $ = useMemoCache(5); + const $ = useMemoCache(2); let a; - if ( - $[0] !== props.a || - $[1] !== props.b || - $[2] !== props.c || - $[3] !== props.d - ) { + if ($[0] !== props) { a = []; a.push(props.a); bb1: { @@ -53,13 +48,10 @@ function Component(props) { } a.push(props.d); - $[0] = props.a; - $[1] = props.b; - $[2] = props.c; - $[3] = props.d; - $[4] = a; + $[0] = props; + $[1] = a; } else { - a = $[4]; + a = $[1]; } return a; } diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-early-return.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-early-return.expect.md index 41f5bc32cc..b39f2c8472 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-early-return.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/conditional-early-return.expect.md @@ -71,10 +71,10 @@ import { unstable_useMemoCache as useMemoCache } from "react"; * props.b does *not* influence `a` */ function ComponentA(props) { - const $ = useMemoCache(5); + const $ = useMemoCache(3); let a_DEBUG; let t0; - if ($[0] !== props.a || $[1] !== props.b || $[2] !== props.d) { + if ($[0] !== props) { t0 = Symbol.for("react.early_return_sentinel"); bb7: { a_DEBUG = []; @@ -86,14 +86,12 @@ function ComponentA(props) { a_DEBUG.push(props.d); } - $[0] = props.a; - $[1] = props.b; - $[2] = props.d; - $[3] = a_DEBUG; - $[4] = t0; + $[0] = props; + $[1] = a_DEBUG; + $[2] = t0; } else { - a_DEBUG = $[3]; - t0 = $[4]; + a_DEBUG = $[1]; + t0 = $[2]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-break-in-scope.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-in-scope.expect.md similarity index 62% rename from compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-break-in-scope.expect.md rename to compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-in-scope.expect.md index 9629dd5cf2..6f490f7f2c 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-break-in-scope.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-in-scope.expect.md @@ -19,6 +19,10 @@ export const FIXTURE_ENTRYPOINT = { sequentialRenders: [ { obj: null, objIsNull: true }, { obj: { a: 2 }, objIsNull: false }, + // check we preserve nullthrows + { obj: { a: undefined }, objIsNull: false }, + { obj: undefined, objIsNull: false }, + { obj: { a: undefined }, objIsNull: false }, ], }; @@ -32,7 +36,7 @@ function useFoo(t0) { const $ = useMemoCache(3); const { obj, objIsNull } = t0; let x; - if ($[0] !== objIsNull || $[1] !== obj.a) { + if ($[0] !== objIsNull || $[1] !== obj) { x = []; bb1: { if (objIsNull) { @@ -42,7 +46,7 @@ function useFoo(t0) { x.push(obj.a); } $[0] = objIsNull; - $[1] = obj.a; + $[1] = obj; $[2] = x; } else { x = $[2]; @@ -56,8 +60,18 @@ export const FIXTURE_ENTRYPOINT = { sequentialRenders: [ { obj: null, objIsNull: true }, { obj: { a: 2 }, objIsNull: false }, + // check we preserve nullthrows + { obj: { a: undefined }, objIsNull: false }, + { obj: undefined, objIsNull: false }, + { obj: { a: undefined }, objIsNull: false }, ], }; ``` - \ No newline at end of file + +### Eval output +(kind: ok) [] +[2] +[null] +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]] +[null] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-break-in-scope.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-in-scope.ts similarity index 66% rename from compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-break-in-scope.ts rename to compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-in-scope.ts index 8db289e9a7..28109d7d68 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-break-in-scope.ts +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-in-scope.ts @@ -15,5 +15,9 @@ export const FIXTURE_ENTRYPOINT = { sequentialRenders: [ { obj: null, objIsNull: true }, { obj: { a: 2 }, objIsNull: false }, + // check we preserve nullthrows + { obj: { a: undefined }, objIsNull: false }, + { obj: undefined, objIsNull: false }, + { obj: { a: undefined }, objIsNull: false }, ], }; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-poisons-outer-scope.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-poisons-outer-scope.expect.md new file mode 100644 index 0000000000..47d7a9fb3b --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-poisons-outer-scope.expect.md @@ -0,0 +1,94 @@ + +## Input + +```javascript +import { identity } from "shared-runtime"; + +function useFoo({ input, cond }) { + const x = []; + label: { + if (cond) { + break label; + } + x.push(identity(input.a.b)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: { a: { b: 2 } }, cond: false }, + // preserve nullthrows + { input: null, cond: false }, + { input: null, cond: true }, + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(t0) { + const $ = useMemoCache(5); + const { input, cond } = t0; + let x; + if ($[0] !== cond || $[1] !== input) { + x = []; + bb1: { + if (cond) { + break bb1; + } + let t1; + if ($[3] !== input.a.b) { + t1 = identity(input.a.b); + $[3] = input.a.b; + $[4] = t1; + } else { + t1 = $[4]; + } + x.push(t1); + } + $[0] = cond; + $[1] = input; + $[2] = x; + } else { + x = $[2]; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: { a: { b: 2 } }, cond: false }, + // preserve nullthrows + { input: null, cond: false }, + { input: null, cond: true }, + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +### Eval output +(kind: ok) [2] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]] +[] +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] +[null] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] +[3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-poisons-outer-scope.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-poisons-outer-scope.ts new file mode 100644 index 0000000000..c1a49e26e1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/break-poisons-outer-scope.ts @@ -0,0 +1,27 @@ +import { identity } from "shared-runtime"; + +function useFoo({ input, cond }) { + const x = []; + label: { + if (cond) { + break label; + } + x.push(identity(input.a.b)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: { a: { b: 2 } }, cond: false }, + // preserve nullthrows + { input: null, cond: false }, + { input: null, cond: true }, + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/loop-break-in-scope.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/loop-break-in-scope.expect.md new file mode 100644 index 0000000000..c46fb1a1c4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/loop-break-in-scope.expect.md @@ -0,0 +1,77 @@ + +## Input + +```javascript +function useFoo({ obj, objIsNull }) { + const x = []; + for (let i = 0; i < 5; i++) { + if (objIsNull) { + continue; + } + x.push(obj.a); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ obj: null, objIsNull: true }], + sequentialRenders: [ + { obj: null, objIsNull: true }, + { obj: { a: 2 }, objIsNull: false }, + // check we preserve nullthrows + { obj: { a: undefined }, objIsNull: false }, + { obj: undefined, objIsNull: false }, + { obj: { a: undefined }, objIsNull: false }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +function useFoo(t0) { + const $ = useMemoCache(3); + const { obj, objIsNull } = t0; + let x; + if ($[0] !== objIsNull || $[1] !== obj) { + x = []; + for (let i = 0; i < 5; i++) { + if (objIsNull) { + continue; + } + + x.push(obj.a); + } + $[0] = objIsNull; + $[1] = obj; + $[2] = x; + } else { + x = $[2]; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ obj: null, objIsNull: true }], + sequentialRenders: [ + { obj: null, objIsNull: true }, + { obj: { a: 2 }, objIsNull: false }, + // check we preserve nullthrows + { obj: { a: undefined }, objIsNull: false }, + { obj: undefined, objIsNull: false }, + { obj: { a: undefined }, objIsNull: false }, + ], +}; + +``` + +### Eval output +(kind: ok) [] +[2,2,2,2,2] +[null,null,null,null,null] +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]] +[null,null,null,null,null] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/loop-break-in-scope.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/loop-break-in-scope.ts new file mode 100644 index 0000000000..f5ecc3380c --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/loop-break-in-scope.ts @@ -0,0 +1,23 @@ +function useFoo({ obj, objIsNull }) { + const x = []; + for (let i = 0; i < 5; i++) { + if (objIsNull) { + continue; + } + x.push(obj.a); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ obj: null, objIsNull: true }], + sequentialRenders: [ + { obj: null, objIsNull: true }, + { obj: { a: 2 }, objIsNull: false }, + // check we preserve nullthrows + { obj: { a: undefined }, objIsNull: false }, + { obj: undefined, objIsNull: false }, + { obj: { a: undefined }, objIsNull: false }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps.expect.md new file mode 100644 index 0000000000..2d1c186cf8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps.expect.md @@ -0,0 +1,114 @@ + +## Input + +```javascript +import { identity } from "shared-runtime"; + +function useFoo({ input, cond, hasAB }) { + const x = []; + if (cond) { + if (!hasAB) { + return null; + } + x.push(identity(input.a.b)); + } else { + x.push(identity(input.a.b)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, cond: true, hasAB: false }], + sequentialRenders: [ + { input: { a: { b: 1 } }, cond: true, hasAB: true }, + { input: null, cond: true, hasAB: false }, + // preserve nullthrows + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + { input: { a: undefined }, cond: true, hasAB: true }, + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + { input: undefined, cond: true, hasAB: true }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(t0) { + const $ = useMemoCache(9); + const { input, cond, hasAB } = t0; + let x; + let t1; + if ($[0] !== cond || $[1] !== hasAB || $[2] !== input) { + t1 = Symbol.for("react.early_return_sentinel"); + bb10: { + x = []; + if (cond) { + if (!hasAB) { + t1 = null; + break bb10; + } + let t2; + if ($[5] !== input.a.b) { + t2 = identity(input.a.b); + $[5] = input.a.b; + $[6] = t2; + } else { + t2 = $[6]; + } + x.push(t2); + } else { + let t2; + if ($[7] !== input.a.b) { + t2 = identity(input.a.b); + $[7] = input.a.b; + $[8] = t2; + } else { + t2 = $[8]; + } + x.push(t2); + } + } + $[0] = cond; + $[1] = hasAB; + $[2] = input; + $[3] = x; + $[4] = t1; + } else { + x = $[3]; + t1 = $[4]; + } + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, cond: true, hasAB: false }], + sequentialRenders: [ + { input: { a: { b: 1 } }, cond: true, hasAB: true }, + { input: null, cond: true, hasAB: false }, + // preserve nullthrows + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + { input: { a: undefined }, cond: true, hasAB: true }, + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + { input: undefined, cond: true, hasAB: true }, + ], +}; + +``` + +### Eval output +(kind: ok) [1] +null +[null] +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] +[null] +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps.ts new file mode 100644 index 0000000000..500d2299aa --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps.ts @@ -0,0 +1,28 @@ +import { identity } from "shared-runtime"; + +function useFoo({ input, cond, hasAB }) { + const x = []; + if (cond) { + if (!hasAB) { + return null; + } + x.push(identity(input.a.b)); + } else { + x.push(identity(input.a.b)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, cond: true, hasAB: false }], + sequentialRenders: [ + { input: { a: { b: 1 } }, cond: true, hasAB: true }, + { input: null, cond: true, hasAB: false }, + // preserve nullthrows + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + { input: { a: undefined }, cond: true, hasAB: true }, + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + { input: undefined, cond: true, hasAB: true }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps1.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps1.expect.md new file mode 100644 index 0000000000..4dfb2abd6e --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps1.expect.md @@ -0,0 +1,123 @@ + +## Input + +```javascript +import { identity } from "shared-runtime"; + +function useFoo({ input, cond, hasAB }) { + const x = []; + if (cond) { + if (!hasAB) { + return null; + } else { + x.push(identity(input.a.b)); + } + x.push(identity(input.a.b)); + } else { + x.push(identity(input.a.b)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, cond: true, hasAB: false }], + sequentialRenders: [ + { input: { a: { b: 1 } }, cond: true, hasAB: true }, + { input: null, cond: true, hasAB: false }, + // preserve nullthrows + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + { input: { a: null }, cond: true, hasAB: true }, + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(t0) { + const $ = useMemoCache(11); + const { input, cond, hasAB } = t0; + let x; + let t1; + if ($[0] !== cond || $[1] !== hasAB || $[2] !== input) { + t1 = Symbol.for("react.early_return_sentinel"); + bb11: { + x = []; + if (cond) { + if (!hasAB) { + t1 = null; + break bb11; + } else { + let t2; + if ($[5] !== input.a.b) { + t2 = identity(input.a.b); + $[5] = input.a.b; + $[6] = t2; + } else { + t2 = $[6]; + } + x.push(t2); + } + let t2; + if ($[7] !== input.a.b) { + t2 = identity(input.a.b); + $[7] = input.a.b; + $[8] = t2; + } else { + t2 = $[8]; + } + x.push(t2); + } else { + let t2; + if ($[9] !== input.a.b) { + t2 = identity(input.a.b); + $[9] = input.a.b; + $[10] = t2; + } else { + t2 = $[10]; + } + x.push(t2); + } + } + $[0] = cond; + $[1] = hasAB; + $[2] = input; + $[3] = x; + $[4] = t1; + } else { + x = $[3]; + t1 = $[4]; + } + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, cond: true, hasAB: false }], + sequentialRenders: [ + { input: { a: { b: 1 } }, cond: true, hasAB: true }, + { input: null, cond: true, hasAB: false }, + // preserve nullthrows + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + { input: { a: null }, cond: true, hasAB: true }, + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + ], +}; + +``` + +### Eval output +(kind: ok) [1,1] +null +[null,null] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] +[null,null] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps1.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps1.ts new file mode 100644 index 0000000000..b0cf445f8e --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/reduce-if-nonexhaustive-poisoned-deps1.ts @@ -0,0 +1,29 @@ +import { identity } from "shared-runtime"; + +function useFoo({ input, cond, hasAB }) { + const x = []; + if (cond) { + if (!hasAB) { + return null; + } else { + x.push(identity(input.a.b)); + } + x.push(identity(input.a.b)); + } else { + x.push(identity(input.a.b)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, cond: true, hasAB: false }], + sequentialRenders: [ + { input: { a: { b: 1 } }, cond: true, hasAB: true }, + { input: null, cond: true, hasAB: false }, + // preserve nullthrows + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + { input: { a: null }, cond: true, hasAB: true }, + { input: { a: { b: undefined } }, cond: true, hasAB: true }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-return-in-scope.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-in-scope.expect.md similarity index 66% rename from compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-return-in-scope.expect.md rename to compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-in-scope.expect.md index 5340758854..a49308233a 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-return-in-scope.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-in-scope.expect.md @@ -17,6 +17,10 @@ export const FIXTURE_ENTRYPOINT = { sequentialRenders: [ { obj: null, objIsNull: true }, { obj: { a: 2 }, objIsNull: false }, + // check we preserve nullthrows + { obj: { a: undefined }, objIsNull: false }, + { obj: undefined, objIsNull: false }, + { obj: { a: undefined }, objIsNull: false }, ], }; @@ -31,7 +35,7 @@ function useFoo(t0) { const { obj, objIsNull } = t0; let x; let t1; - if ($[0] !== objIsNull || $[1] !== obj.b) { + if ($[0] !== objIsNull || $[1] !== obj) { t1 = Symbol.for("react.early_return_sentinel"); bb7: { x = []; @@ -43,7 +47,7 @@ function useFoo(t0) { x.push(obj.b); } $[0] = objIsNull; - $[1] = obj.b; + $[1] = obj; $[2] = x; $[3] = t1; } else { @@ -62,8 +66,18 @@ export const FIXTURE_ENTRYPOINT = { sequentialRenders: [ { obj: null, objIsNull: true }, { obj: { a: 2 }, objIsNull: false }, + // check we preserve nullthrows + { obj: { a: undefined }, objIsNull: false }, + { obj: undefined, objIsNull: false }, + { obj: { a: undefined }, objIsNull: false }, ], }; ``` - \ No newline at end of file + +### Eval output +(kind: ok) +[null] +[null] +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] +[null] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-return-in-scope.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-in-scope.ts similarity index 64% rename from compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-return-in-scope.ts rename to compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-in-scope.ts index 25a4b75eb7..fd406aa596 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug-reduce-reactive-deps-return-in-scope.ts +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-in-scope.ts @@ -13,5 +13,9 @@ export const FIXTURE_ENTRYPOINT = { sequentialRenders: [ { obj: null, objIsNull: true }, { obj: { a: 2 }, objIsNull: false }, + // check we preserve nullthrows + { obj: { a: undefined }, objIsNull: false }, + { obj: undefined, objIsNull: false }, + { obj: { a: undefined }, objIsNull: false }, ], }; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-poisons-outer-scope.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-poisons-outer-scope.expect.md new file mode 100644 index 0000000000..1a3e7471cc --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-poisons-outer-scope.expect.md @@ -0,0 +1,100 @@ + +## Input + +```javascript +import { identity } from "shared-runtime"; + +function useFoo({ input, cond }) { + const x = []; + if (cond) { + return null; + } + x.push(identity(input.a.b)); + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: { a: { b: 2 } }, cond: false }, + // preserve nullthrows + { input: null, cond: false }, + { input: null, cond: true }, + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(t0) { + const $ = useMemoCache(6); + const { input, cond } = t0; + let x; + let t1; + if ($[0] !== cond || $[1] !== input) { + t1 = Symbol.for("react.early_return_sentinel"); + bb7: { + x = []; + if (cond) { + t1 = null; + break bb7; + } + let t2; + if ($[4] !== input.a.b) { + t2 = identity(input.a.b); + $[4] = input.a.b; + $[5] = t2; + } else { + t2 = $[5]; + } + x.push(t2); + } + $[0] = cond; + $[1] = input; + $[2] = x; + $[3] = t1; + } else { + x = $[2]; + t1 = $[3]; + } + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: { a: { b: 2 } }, cond: false }, + // preserve nullthrows + { input: null, cond: false }, + { input: null, cond: true }, + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +### Eval output +(kind: ok) [2] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]] +null +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] +[null] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] +[3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-poisons-outer-scope.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-poisons-outer-scope.ts new file mode 100644 index 0000000000..81aff0f640 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-poisoned/return-poisons-outer-scope.ts @@ -0,0 +1,25 @@ +import { identity } from "shared-runtime"; + +function useFoo({ input, cond }) { + const x = []; + if (cond) { + return null; + } + x.push(identity(input.a.b)); + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: { a: { b: 2 } }, cond: false }, + // preserve nullthrows + { input: null, cond: false }, + { input: null, cond: true }, + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/else-branch-scope-unpoisoned.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/else-branch-scope-unpoisoned.expect.md new file mode 100644 index 0000000000..491342c5c9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/else-branch-scope-unpoisoned.expect.md @@ -0,0 +1,94 @@ + +## Input + +```javascript +import { identity } from "shared-runtime"; + +function useFoo({ input, cond }) { + const x = []; + label: { + if (cond) { + break label; + } else { + x.push(identity(input.a.b)); + } + } + return x[0]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: null, cond: true }, + { input: { a: { b: 2 } }, cond: false }, + { input: null, cond: true }, + // preserve nullthrows + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(t0) { + const $ = useMemoCache(5); + const { input, cond } = t0; + let x; + if ($[0] !== cond || $[1] !== input) { + x = []; + bb1: if (cond) { + break bb1; + } else { + let t1; + if ($[3] !== input.a.b) { + t1 = identity(input.a.b); + $[3] = input.a.b; + $[4] = t1; + } else { + t1 = $[4]; + } + x.push(t1); + } + $[0] = cond; + $[1] = input; + $[2] = x; + } else { + x = $[2]; + } + return x[0]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: null, cond: true }, + { input: { a: { b: 2 } }, cond: false }, + { input: null, cond: true }, + // preserve nullthrows + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +### Eval output +(kind: ok) +2 + +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] +null +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] +3 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/else-branch-scope-unpoisoned.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/else-branch-scope-unpoisoned.ts new file mode 100644 index 0000000000..299e662063 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/else-branch-scope-unpoisoned.ts @@ -0,0 +1,28 @@ +import { identity } from "shared-runtime"; + +function useFoo({ input, cond }) { + const x = []; + label: { + if (cond) { + break label; + } else { + x.push(identity(input.a.b)); + } + } + return x[0]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: null, cond: true }, + { input: { a: { b: 2 } }, cond: false }, + { input: null, cond: true }, + // preserve nullthrows + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-label.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-label.expect.md new file mode 100644 index 0000000000..a8a8c58997 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-label.expect.md @@ -0,0 +1,81 @@ + +## Input + +```javascript +function useFoo({ input, cond }) { + const x = []; + label: { + if (cond) { + break label; + } + } + x.push(input.a.b); // unconditional + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: { a: { b: 2 } }, cond: false }, + // preserve nullthrows + { input: null, cond: false }, + { input: null, cond: true }, + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +function useFoo(t0) { + const $ = useMemoCache(3); + const { input, cond } = t0; + let x; + if ($[0] !== cond || $[1] !== input.a.b) { + x = []; + bb1: if (cond) { + break bb1; + } + + x.push(input.a.b); + $[0] = cond; + $[1] = input.a.b; + $[2] = x; + } else { + x = $[2]; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: { a: { b: 2 } }, cond: false }, + // preserve nullthrows + { input: null, cond: false }, + { input: null, cond: true }, + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +### Eval output +(kind: ok) [2] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]] +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] +[null] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] +[3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-label.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-label.ts new file mode 100644 index 0000000000..b0348656cf --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-label.ts @@ -0,0 +1,25 @@ +function useFoo({ input, cond }) { + const x = []; + label: { + if (cond) { + break label; + } + } + x.push(input.a.b); // unconditional + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: { a: { b: 2 } }, cond: false }, + // preserve nullthrows + { input: null, cond: false }, + { input: null, cond: true }, + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-loop-break.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-loop-break.expect.md new file mode 100644 index 0000000000..58c92367cc --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-loop-break.expect.md @@ -0,0 +1,86 @@ + +## Input + +```javascript +function useFoo({ input, max }) { + const x = []; + let i = 0; + while (true) { + i += 1; + if (i > max) { + break; + } + } + x.push(i); + x.push(input.a.b); // unconditional + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, max: 8 }], + sequentialRenders: [ + { input: { a: { b: 2 } }, max: 8 }, + // preserve nullthrows + { input: null, max: 8 }, + { input: {}, max: 8 }, + { input: { a: { b: null } }, max: 8 }, + { input: { a: null }, max: 8 }, + { input: { a: { b: 3 } }, max: 8 }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +function useFoo(t0) { + const $ = useMemoCache(3); + const { input, max } = t0; + let x; + if ($[0] !== max || $[1] !== input.a.b) { + x = []; + let i = 0; + while (true) { + i = i + 1; + if (i > max) { + break; + } + } + + x.push(i); + x.push(input.a.b); + $[0] = max; + $[1] = input.a.b; + $[2] = x; + } else { + x = $[2]; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, max: 8 }], + sequentialRenders: [ + { input: { a: { b: 2 } }, max: 8 }, + // preserve nullthrows + { input: null, max: 8 }, + { input: {}, max: 8 }, + { input: { a: { b: null } }, max: 8 }, + { input: { a: null }, max: 8 }, + { input: { a: { b: 3 } }, max: 8 }, + ], +}; + +``` + +### Eval output +(kind: ok) [9,2] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]] +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] +[9,null] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] +[9,3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-loop-break.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-loop-break.ts new file mode 100644 index 0000000000..e3936441ba --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/jump-target-within-scope-loop-break.ts @@ -0,0 +1,27 @@ +function useFoo({ input, max }) { + const x = []; + let i = 0; + while (true) { + i += 1; + if (i > max) { + break; + } + } + x.push(i); + x.push(input.a.b); // unconditional + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, max: 8 }], + sequentialRenders: [ + { input: { a: { b: 2 } }, max: 8 }, + // preserve nullthrows + { input: null, max: 8 }, + { input: {}, max: 8 }, + { input: { a: { b: null } }, max: 8 }, + { input: { a: null }, max: 8 }, + { input: { a: { b: 3 } }, max: 8 }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps.expect.md new file mode 100644 index 0000000000..b8feeb03cc --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps.expect.md @@ -0,0 +1,91 @@ + +## Input + +```javascript +import { identity } from "shared-runtime"; + +function useFoo({ input, hasAB, returnNull }) { + const x = []; + if (!hasAB) { + x.push(identity(input.a)); + if (!returnNull) { + return null; + } + } else { + x.push(identity(input.a.b)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, hasAB: false, returnNull: false }], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(t0) { + const $ = useMemoCache(9); + const { input, hasAB, returnNull } = t0; + let x; + let t1; + if ($[0] !== hasAB || $[1] !== input.a || $[2] !== returnNull) { + t1 = Symbol.for("react.early_return_sentinel"); + bb10: { + x = []; + if (!hasAB) { + let t2; + if ($[5] !== input.a) { + t2 = identity(input.a); + $[5] = input.a; + $[6] = t2; + } else { + t2 = $[6]; + } + x.push(t2); + if (!returnNull) { + t1 = null; + break bb10; + } + } else { + let t2; + if ($[7] !== input.a.b) { + t2 = identity(input.a.b); + $[7] = input.a.b; + $[8] = t2; + } else { + t2 = $[8]; + } + x.push(t2); + } + } + $[0] = hasAB; + $[1] = input.a; + $[2] = returnNull; + $[3] = x; + $[4] = t1; + } else { + x = $[3]; + t1 = $[4]; + } + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, hasAB: false, returnNull: false }], +}; + +``` + +### Eval output +(kind: ok) null \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps.ts new file mode 100644 index 0000000000..4bb83141cd --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps.ts @@ -0,0 +1,19 @@ +import { identity } from "shared-runtime"; + +function useFoo({ input, hasAB, returnNull }) { + const x = []; + if (!hasAB) { + x.push(identity(input.a)); + if (!returnNull) { + return null; + } + } else { + x.push(identity(input.a.b)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, hasAB: false, returnNull: false }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps1.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps1.expect.md new file mode 100644 index 0000000000..3ba36fc1ed --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps1.expect.md @@ -0,0 +1,123 @@ + +## Input + +```javascript +import { identity } from "shared-runtime"; + +function useFoo({ input, cond2, cond1 }) { + const x = []; + if (cond1) { + if (!cond2) { + x.push(identity(input.a.b)); + return null; + } else { + x.push(identity(input.a.b)); + } + } else { + x.push(identity(input.a.b)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, cond1: true, cond2: false }], + sequentialRenders: [ + { input: { a: { b: 1 } }, cond1: true, cond2: true }, + { input: null, cond1: true, cond2: false }, + // preserve nullthrows + { input: { a: { b: undefined } }, cond1: true, cond2: true }, + { input: { a: null }, cond1: true, cond2: true }, + { input: { a: { b: undefined } }, cond1: true, cond2: true }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(t0) { + const $ = useMemoCache(11); + const { input, cond2, cond1 } = t0; + let x; + let t1; + if ($[0] !== cond1 || $[1] !== cond2 || $[2] !== input.a.b) { + t1 = Symbol.for("react.early_return_sentinel"); + bb11: { + x = []; + if (cond1) { + if (!cond2) { + let t2; + if ($[5] !== input.a.b) { + t2 = identity(input.a.b); + $[5] = input.a.b; + $[6] = t2; + } else { + t2 = $[6]; + } + x.push(t2); + t1 = null; + break bb11; + } else { + let t2; + if ($[7] !== input.a.b) { + t2 = identity(input.a.b); + $[7] = input.a.b; + $[8] = t2; + } else { + t2 = $[8]; + } + x.push(t2); + } + } else { + let t2; + if ($[9] !== input.a.b) { + t2 = identity(input.a.b); + $[9] = input.a.b; + $[10] = t2; + } else { + t2 = $[10]; + } + x.push(t2); + } + } + $[0] = cond1; + $[1] = cond2; + $[2] = input.a.b; + $[3] = x; + $[4] = t1; + } else { + x = $[3]; + t1 = $[4]; + } + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, cond1: true, cond2: false }], + sequentialRenders: [ + { input: { a: { b: 1 } }, cond1: true, cond2: true }, + { input: null, cond1: true, cond2: false }, + // preserve nullthrows + { input: { a: { b: undefined } }, cond1: true, cond2: true }, + { input: { a: null }, cond1: true, cond2: true }, + { input: { a: { b: undefined } }, cond1: true, cond2: true }, + ], +}; + +``` + +### Eval output +(kind: ok) [1] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'a') ]] +[null] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] +[null] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps1.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps1.ts new file mode 100644 index 0000000000..1737b0f75f --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/reduce-if-exhaustive-nonpoisoned-deps1.ts @@ -0,0 +1,29 @@ +import { identity } from "shared-runtime"; + +function useFoo({ input, cond2, cond1 }) { + const x = []; + if (cond1) { + if (!cond2) { + x.push(identity(input.a.b)); + return null; + } else { + x.push(identity(input.a.b)); + } + } else { + x.push(identity(input.a.b)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, cond1: true, cond2: false }], + sequentialRenders: [ + { input: { a: { b: 1 } }, cond1: true, cond2: true }, + { input: null, cond1: true, cond2: false }, + // preserve nullthrows + { input: { a: { b: undefined } }, cond1: true, cond2: true }, + { input: { a: null }, cond1: true, cond2: true }, + { input: { a: { b: undefined } }, cond1: true, cond2: true }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/return-before-scope-starts.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/return-before-scope-starts.expect.md new file mode 100644 index 0000000000..f259fd5053 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/return-before-scope-starts.expect.md @@ -0,0 +1,90 @@ + +## Input + +```javascript +import { arrayPush } from "shared-runtime"; + +function useFoo({ input, cond }) { + if (cond) { + return { result: "early return" }; + } + + // unconditional + const x = []; + arrayPush(x, input.a.b); + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: null, cond: true }, + { input: { a: { b: 2 } }, cond: false }, + { input: null, cond: true }, + // preserve nullthrows + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { arrayPush } from "shared-runtime"; + +function useFoo(t0) { + const $ = useMemoCache(3); + const { input, cond } = t0; + if (cond) { + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = { result: "early return" }; + $[0] = t1; + } else { + t1 = $[0]; + } + return t1; + } + let x; + if ($[1] !== input.a.b) { + x = []; + arrayPush(x, input.a.b); + $[1] = input.a.b; + $[2] = x; + } else { + x = $[2]; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: null, cond: true }, + { input: { a: { b: 2 } }, cond: false }, + { input: null, cond: true }, + // preserve nullthrows + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +### Eval output +(kind: ok) {"result":"early return"} +[2] +{"result":"early return"} +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] +[null] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] +[3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/return-before-scope-starts.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/return-before-scope-starts.ts new file mode 100644 index 0000000000..4641468db5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/return-before-scope-starts.ts @@ -0,0 +1,27 @@ +import { arrayPush } from "shared-runtime"; + +function useFoo({ input, cond }) { + if (cond) { + return { result: "early return" }; + } + + // unconditional + const x = []; + arrayPush(x, input.a.b); + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: null, cond: true }, + { input: { a: { b: 2 } }, cond: false }, + { input: null, cond: true }, + // preserve nullthrows + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.expect.md new file mode 100644 index 0000000000..15116d7789 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +import { arrayPush } from "shared-runtime"; + +function useFoo({ input, cond }) { + if (cond) { + throw new Error("throw with error!"); + } + + // unconditional + const x = []; + arrayPush(x, input.a.b); + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: null, cond: true }, + { input: { a: { b: 2 } }, cond: false }, + { input: null, cond: true }, + // preserve nullthrows + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { arrayPush } from "shared-runtime"; + +function useFoo(t0) { + const $ = useMemoCache(2); + const { input, cond } = t0; + if (cond) { + throw new Error("throw with error!"); + } + let x; + if ($[0] !== input.a.b) { + x = []; + arrayPush(x, input.a.b); + $[0] = input.a.b; + $[1] = x; + } else { + x = $[1]; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: null, cond: true }, + { input: { a: { b: 2 } }, cond: false }, + { input: null, cond: true }, + // preserve nullthrows + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; + +``` + +### Eval output +(kind: ok) [[ (exception in render) Error: throw with error! ]] +[[ (exception in render) Error: throw with error! ]] +[[ (exception in render) Error: throw with error! ]] +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] +[null] +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] +[3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.ts new file mode 100644 index 0000000000..4366a4016c --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.ts @@ -0,0 +1,27 @@ +import { arrayPush } from "shared-runtime"; + +function useFoo({ input, cond }) { + if (cond) { + throw new Error("throw with error!"); + } + + // unconditional + const x = []; + arrayPush(x, input.a.b); + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { a: { b: 2 } }, cond: false }], + sequentialRenders: [ + { input: null, cond: true }, + { input: { a: { b: 2 } }, cond: false }, + { input: null, cond: true }, + // preserve nullthrows + { input: {}, cond: false }, + { input: { a: { b: null } }, cond: false }, + { input: { a: null }, cond: false }, + { input: { a: { b: 3 } }, cond: false }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/reduce-if-exhaustive-poisoned-deps.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/reduce-if-exhaustive-poisoned-deps.expect.md new file mode 100644 index 0000000000..df9e6017c9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/reduce-if-exhaustive-poisoned-deps.expect.md @@ -0,0 +1,101 @@ + +## Input + +```javascript +import { identity } from "shared-runtime"; + +function useFoo({ input, inputHasAB, inputHasABC }) { + const x = []; + if (!inputHasABC) { + x.push(identity(input.a)); + if (!inputHasAB) { + return null; + } + x.push(identity(input.a.b)); + } else { + x.push(identity(input.a.b.c)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, inputHasAB: false, inputHasABC: false }], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(t0) { + const $ = useMemoCache(11); + const { input, inputHasAB, inputHasABC } = t0; + let x; + let t1; + if ($[0] !== inputHasABC || $[1] !== input.a || $[2] !== inputHasAB) { + t1 = Symbol.for("react.early_return_sentinel"); + bb10: { + x = []; + if (!inputHasABC) { + let t2; + if ($[5] !== input.a) { + t2 = identity(input.a); + $[5] = input.a; + $[6] = t2; + } else { + t2 = $[6]; + } + x.push(t2); + if (!inputHasAB) { + t1 = null; + break bb10; + } + let t3; + if ($[7] !== input.a.b) { + t3 = identity(input.a.b); + $[7] = input.a.b; + $[8] = t3; + } else { + t3 = $[8]; + } + x.push(t3); + } else { + let t2; + if ($[9] !== input.a.b.c) { + t2 = identity(input.a.b.c); + $[9] = input.a.b.c; + $[10] = t2; + } else { + t2 = $[10]; + } + x.push(t2); + } + } + $[0] = inputHasABC; + $[1] = input.a; + $[2] = inputHasAB; + $[3] = x; + $[4] = t1; + } else { + x = $[3]; + t1 = $[4]; + } + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, inputHasAB: false, inputHasABC: false }], +}; + +``` + +### Eval output +(kind: ok) null \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/reduce-if-exhaustive-poisoned-deps.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/reduce-if-exhaustive-poisoned-deps.ts new file mode 100644 index 0000000000..55a3cb4c34 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reduce-reactive-deps/reduce-if-exhaustive-poisoned-deps.ts @@ -0,0 +1,20 @@ +import { identity } from "shared-runtime"; + +function useFoo({ input, inputHasAB, inputHasABC }) { + const x = []; + if (!inputHasABC) { + x.push(identity(input.a)); + if (!inputHasAB) { + return null; + } + x.push(identity(input.a.b)); + } else { + x.push(identity(input.a.b.c)); + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ input: { b: 1 }, inputHasAB: false, inputHasABC: false }], +};