Repository navigation
Conversation
…tio basis Fixes react#2038. distributeFreeSpaceFirstPass subtracted the frozen item's shrink-scaled factor inline, which the compiler contracts into a fused multiply-add with the unrounded product. It therefore did not cancel against the float accumulator built in calculateFlexLine and left a 2^-19 residual that distributeFreeSpaceSecondPass divided the remaining free space by, measuring the item at ~8.3e9 instead of 0. The absolute 1e-6f guard from react#1974 does not cover it. Round the product to float before subtracting, and replace the epsilon with an exact count of shrinkable items left on the line. 🤖 Co-authored-by: Claude <noreply@anthropic.com>
intergalacticspacehighway
added a commit
to intergalacticspacehighway/yoga
that referenced
this pull request
Oct 18, 2026
Collaborator
|
Thanks @sebholl for the analysis and repro! I've opened #2044, which sums the shrink factors of the unfrozen items instead of subtracting the frozen ones. That makes the total itself exact (0 when every item is frozen), so no separate count is needed, and it also fixes #931. I've also included your aspectRatio tests there. |
Author
|
Closing in favor of @intergalacticspacehighway 's fix. |
meta-codesync Bot
pushed a commit
that referenced
this pull request
Oct 20, 2026
…esidual Fixes #2038 Fixes #1651 Closes #2039 Closes #1855 Thanks to @sebholl for the analysis and the reproduction in #2038. ## Issue When a flex line overflows, the first pass freezes the items that hit their minimum size and removes them from the shrink total. The second pass then divides the remaining free space by that total. The total was computed by subtracting the frozen items flex shrink factors from the float total. When every item freezes, the total should become 0, but float subtraction leaves a small residual. ### Example All items have `flexShrink: 1`: ``` row (width: 350, flexDirection: row) ├── A width: 100.1 minWidth: 90 ├── B width: 200.3 minWidth: 190 └── C width: 300.7 minWidth: 290 ``` The items need 601.1 but the row is only 350, so they must shrink by 251.1. Each item would go below its `minWidth`, so the first pass freezes all three (at 90 / 190 / 290) and removes them from the shrink total. With no items left, the total should be 0. But in float, `-601.1 + 100.1 + 200.3 + 300.7 = 0.000061`. That is above the `1e-6` guard added in #1974 (for #1665), so the second pass divides the remaining free space by it: `-220 / 0.000061 ≈ -3.6 million`. Instead of staying at their min, the items grow to about **361M / 722M / 1084M** (expected **90 / 190 / 290**). ### Solution With non-negative `flexShrink`, every scaled shrink factor is ≤ 0, so a real total is never positive. A positive total can only be this float residual, and dividing by it is what makes the items explode. The second pass now treats a positive total like the near-zero case of the `1e-6` guard from #1974 and uses the existing fallback. Negative residuals (which the min clamp already handles) and all other layouts are unchanged. Fixing the fallback for `flexShrink < 1` (#931) is left for a separate PR. #### Why not `inexactEquals` (|total| < 0.0001)? The leftover grows with the size of the total. For example, 60 chips of about 100pt each in a 1000pt row give a total of about 6000, and the leftover is then often larger than 0.0001. In a test with rows like this, about half of them still exploded with `inexactEquals`. With the sign check, none did, because a real shrink total is never positive. Differential Revision: D126038629 Pull Request resolved: #2044
meta-codesync Bot
pushed a commit
to facebook/litho
that referenced
this pull request
Oct 20, 2026
…esidual Summary: Fixes react/yoga#2038 Fixes react/yoga#1651 Closes react/yoga#2039 Closes react/yoga#1855 Thanks to sebholl for the analysis and the reproduction in react/yoga#2038. ## Issue When a flex line overflows, the first pass freezes the items that hit their minimum size and removes them from the shrink total. The second pass then divides the remaining free space by that total. The total was computed by subtracting the frozen items flex shrink factors from the float total. When every item freezes, the total should become 0, but float subtraction leaves a small residual. ### Example All items have `flexShrink: 1`: ``` row (width: 350, flexDirection: row) ├── A width: 100.1 minWidth: 90 ├── B width: 200.3 minWidth: 190 └── C width: 300.7 minWidth: 290 ``` The items need 601.1 but the row is only 350, so they must shrink by 251.1. Each item would go below its `minWidth`, so the first pass freezes all three (at 90 / 190 / 290) and removes them from the shrink total. With no items left, the total should be 0. But in float, `-601.1 + 100.1 + 200.3 + 300.7 = 0.000061`. That is above the `1e-6` guard added in react/yoga#1974 (for react/yoga#1665), so the second pass divides the remaining free space by it: `-220 / 0.000061 ≈ -3.6 million`. Instead of staying at their min, the items grow to about **361M / 722M / 1084M** (expected **90 / 190 / 290**). ### Solution With non-negative `flexShrink`, every scaled shrink factor is ≤ 0, so a real total is never positive. A positive total can only be this float residual, and dividing by it is what makes the items explode. The second pass now treats a positive total like the near-zero case of the `1e-6` guard from react/yoga#1974 and uses the existing fallback. Negative residuals (which the min clamp already handles) and all other layouts are unchanged. Fixing the fallback for `flexShrink < 1` (react/yoga#931) is left for a separate PR. #### Why not `inexactEquals` (|total| < 0.0001)? The leftover grows with the size of the total. For example, 60 chips of about 100pt each in a 1000pt row give a total of about 6000, and the leftover is then often larger than 0.0001. In a test with rows like this, about half of them still exploded with `inexactEquals`. With the sign check, none did, because a real shrink total is never positive. X-link: react/yoga#2044 Reviewed By: javache Differential Revision: D126038629 Pulled By: cortinico fbshipit-source-id: dc1ec3370783e227ce4477bcd632ae5907034193
meta-codesync Bot
pushed a commit
to react/react-native
that referenced
this pull request
Oct 20, 2026
…esidual Summary: Fixes react/yoga#2038 Fixes react/yoga#1651 Closes react/yoga#2039 Closes react/yoga#1855 Thanks to sebholl for the analysis and the reproduction in react/yoga#2038. ## Issue When a flex line overflows, the first pass freezes the items that hit their minimum size and removes them from the shrink total. The second pass then divides the remaining free space by that total. The total was computed by subtracting the frozen items flex shrink factors from the float total. When every item freezes, the total should become 0, but float subtraction leaves a small residual. ### Example All items have `flexShrink: 1`: ``` row (width: 350, flexDirection: row) ├── A width: 100.1 minWidth: 90 ├── B width: 200.3 minWidth: 190 └── C width: 300.7 minWidth: 290 ``` The items need 601.1 but the row is only 350, so they must shrink by 251.1. Each item would go below its `minWidth`, so the first pass freezes all three (at 90 / 190 / 290) and removes them from the shrink total. With no items left, the total should be 0. But in float, `-601.1 + 100.1 + 200.3 + 300.7 = 0.000061`. That is above the `1e-6` guard added in react/yoga#1974 (for react/yoga#1665), so the second pass divides the remaining free space by it: `-220 / 0.000061 ≈ -3.6 million`. Instead of staying at their min, the items grow to about **361M / 722M / 1084M** (expected **90 / 190 / 290**). ### Solution With non-negative `flexShrink`, every scaled shrink factor is ≤ 0, so a real total is never positive. A positive total can only be this float residual, and dividing by it is what makes the items explode. The second pass now treats a positive total like the near-zero case of the `1e-6` guard from react/yoga#1974 and uses the existing fallback. Negative residuals (which the min clamp already handles) and all other layouts are unchanged. Fixing the fallback for `flexShrink < 1` (react/yoga#931) is left for a separate PR. #### Why not `inexactEquals` (|total| < 0.0001)? The leftover grows with the size of the total. For example, 60 chips of about 100pt each in a 1000pt row give a total of about 6000, and the leftover is then often larger than 0.0001. In a test with rows like this, about half of them still exploded with `inexactEquals`. With the sign check, none did, because a real shrink total is never positive. X-link: react/yoga#2044 Reviewed By: javache Differential Revision: D126038629 Pulled By: cortinico fbshipit-source-id: dc1ec3370783e227ce4477bcd632ae5907034193
cipolleschi
pushed a commit
to react/react-native
that referenced
this pull request
Oct 26, 2026
…esidual Summary: Fixes react/yoga#2038 Fixes react/yoga#1651 Closes react/yoga#2039 Closes react/yoga#1855 Thanks to sebholl for the analysis and the reproduction in react/yoga#2038. ## Issue When a flex line overflows, the first pass freezes the items that hit their minimum size and removes them from the shrink total. The second pass then divides the remaining free space by that total. The total was computed by subtracting the frozen items flex shrink factors from the float total. When every item freezes, the total should become 0, but float subtraction leaves a small residual. ### Example All items have `flexShrink: 1`: ``` row (width: 350, flexDirection: row) ├── A width: 100.1 minWidth: 90 ├── B width: 200.3 minWidth: 190 └── C width: 300.7 minWidth: 290 ``` The items need 601.1 but the row is only 350, so they must shrink by 251.1. Each item would go below its `minWidth`, so the first pass freezes all three (at 90 / 190 / 290) and removes them from the shrink total. With no items left, the total should be 0. But in float, `-601.1 + 100.1 + 200.3 + 300.7 = 0.000061`. That is above the `1e-6` guard added in react/yoga#1974 (for react/yoga#1665), so the second pass divides the remaining free space by it: `-220 / 0.000061 ≈ -3.6 million`. Instead of staying at their min, the items grow to about **361M / 722M / 1084M** (expected **90 / 190 / 290**). ### Solution With non-negative `flexShrink`, every scaled shrink factor is ≤ 0, so a real total is never positive. A positive total can only be this float residual, and dividing by it is what makes the items explode. The second pass now treats a positive total like the near-zero case of the `1e-6` guard from react/yoga#1974 and uses the existing fallback. Negative residuals (which the min clamp already handles) and all other layouts are unchanged. Fixing the fallback for `flexShrink < 1` (react/yoga#931) is left for a separate PR. #### Why not `inexactEquals` (|total| < 0.0001)? The leftover grows with the size of the total. For example, 60 chips of about 100pt each in a 1000pt row give a total of about 6000, and the leftover is then often larger than 0.0001. In a test with rows like this, about half of them still exploded with `inexactEquals`. With the sign check, none did, because a real shrink total is never positive. X-link: react/yoga#2044 Reviewed By: javache Differential Revision: D126038629 Pulled By: cortinico fbshipit-source-id: dc1ec3370783e227ce4477bcd632ae5907034193 (cherry picked from commit 24a3be5)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #2038.
When the only shrinkable item on an overflowing row has its flex basis derived from
aspectRatio,distributeFreeSpaceSecondPassdivides the remaining free space by a2^-19residual left intotalFlexShrinkScaledFactors, and the item is measured at ~8.3e9 instead of 0. Sibling of #1665 / #1974 / #1783; the absolute1e-6fguard from #1974 does not catch this residual.Mechanism (full trace in #2038):
calculateFlexLineaccumulatesround(-3 * 21.083334f) = -63.25.distributeFreeSpaceFirstPassthen subtracts the same product inline, which clang contracts into a fused multiply-add using the unrounded product63.2500019, leaving exactly2^-19 = 1.9e-6. Building with-ffp-contract=offmakes the bug disappear, which is also why it never shows in the wasm playground.Two changes:
distributeFreeSpaceFirstPass: round the excluded product tofloatbefore subtracting it. This is the cause, and on its own it fixes the repro under the default-ffp-contract=on.1e-6fepsilon indistributeFreeSpaceSecondPasswith an exact count of shrinkable items left on the line (FlexLineRunningLayout::numberOfShrinkableItems, kept in lockstep with the total). "Are any shrinkable items left?" is a counting question, and no absolute epsilon is right because the residual scales withflexShrink * flexBasis. This also keeps the fix correct under-ffp-contract=fast, where (1) alone can still be contracted across statements.Left alone deliberately: the first-pass division (
remainingFreeSpace / totalFlexShrinkScaledFactorsinsidedistributeFreeSpaceFirstPass) is structurally exposed to the same issue if an item freezes mid-loop, but I have no repro for it, and a count-based guard cannot apply there because the current item is not yet frozen. Happy to add something if a maintainer prefers.Test plan
tests/YGFlexShrinkAspectRatioBugTest.cpp(3 tests): the 4-node repro from flexShrink runaway (~8e9) when the sole shrinkable item has an aspectRatio basis; FMA residual slips past the 1e-6f guard from #1974 #2038, a sweep over shrink factors that currently diverge, and a basis guard. The first two fail onmainand pass with this change.clang++ -std=c++20 -O2(Apple clang, arm64).-ffp-contract=fast.YGFlexShrinkBorderBugTest(flexBasis+flexShrink+borderWidth result in an wrong value #1665) still passes.