diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index f6872da1117..20f1a18b1ee 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -2596,11 +2596,17 @@ function lowerExpression( // Store the previous value to a temporary const previousValuePlace = lowerValueToTemporary(builder, value); + const capturedPreviousValue = lowerValueToTemporary(builder, { + kind: 'LoadLocal', + place: {...previousValuePlace}, + loc: exprLoc, + }); + // Store the new value to a temporary const updatedValue = lowerValueToTemporary(builder, { kind: 'BinaryExpression', operator: binaryOperator, - left: {...previousValuePlace}, + left: {...capturedPreviousValue}, right: lowerValueToTemporary(builder, { kind: 'Primitive', value: 1, @@ -2633,7 +2639,7 @@ function lowerExpression( kind: 'LoadLocal', place: expr.node.prefix ? {...newValuePlace} - : {...previousValuePlace}, + : {...capturedPreviousValue}, loc: exprLoc, }; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-post-increment-assignment.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-post-increment-assignment.expect.md new file mode 100644 index 00000000000..6023b04622e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-post-increment-assignment.expect.md @@ -0,0 +1,79 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel:false +/** + * Bug repro: + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) {"count":3,"res":[0,1,2]} + * Forget: + * (kind: ok) {"count":3,"res":[1,2,3]} + * + * The post-increment operator `agg.count++` should return the value + * BEFORE incrementing, but the compiler's optimization incorrectly + * causes the incremented value to be used. + */ +function Component(props) { + const items = [0, 1, 2]; + return items.reduce((agg, item) => { + const current = agg.count++; + agg.res.push(current); + return agg; + }, {count: 0, res: []}); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false +/** + * Bug repro: + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) {"count":3,"res":[0,1,2]} + * Forget: + * (kind: ok) {"count":3,"res":[1,2,3]} + * + * The post-increment operator `agg.count++` should return the value + * BEFORE incrementing, but the compiler's optimization incorrectly + * causes the incremented value to be used. + */ +function Component(props) { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const items = [0, 1, 2]; + t0 = items.reduce( + _temp, + + { count: 0, res: [] }, + ); + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp(agg, item) { + agg.count = agg.count + 1; + const current = agg.count; + agg.res.push(current); + return agg; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-post-increment-assignment.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-post-increment-assignment.js new file mode 100644 index 00000000000..40fc16d603f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-post-increment-assignment.js @@ -0,0 +1,26 @@ +// @enableNewMutationAliasingModel:false +/** + * Bug repro: + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) {"count":3,"res":[0,1,2]} + * Forget: + * (kind: ok) {"count":3,"res":[1,2,3]} + * + * The post-increment operator `agg.count++` should return the value + * BEFORE incrementing, but the compiler's optimization incorrectly + * causes the incremented value to be used. + */ +function Component(props) { + const items = [0, 1, 2]; + return items.reduce((agg, item) => { + const current = agg.count++; + agg.res.push(current); + return agg; + }, {count: 0, res: []}); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts index 531c3cf27f6..76c153cfb35 100644 --- a/compiler/packages/snap/src/SproutTodoFilter.ts +++ b/compiler/packages/snap/src/SproutTodoFilter.ts @@ -460,6 +460,7 @@ const skipFilter = new Set([ 'fbt/bug-fbt-plural-multiple-function-calls', 'fbt/bug-fbt-plural-multiple-mixed-call-tag', 'bug-invalid-phi-as-dependency', + 'bug-post-increment-assignment', 'bug-ref-prefix-postfix-operator', // 'react-compiler-runtime' not yet supported