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 }], +};