Skip to content

Fix stack overflow with many attributed runs#1

Closed
bisonbet wants to merge 1 commit into
mainfrom
pr-57-fix-stack-overflow
Closed

Fix stack overflow with many attributed runs#1
bisonbet wants to merge 1 commit into
mainfrom
pr-57-fix-stack-overflow

Conversation

@bisonbet

Copy link
Copy Markdown
Owner

Cherry-picked from gonzalezreal/textual PR gonzalezreal#57

Fixes stack overflow when rendering text with many attributed runs (e.g., emoji-dense text). Switches from Text interpolation to Text concatenation to avoid deep nesting that causes stack overflow in LocalizedStringKey.resolve.

The reduce in TextBuilder built the result via Text("\(partialResult)\(text)"),
wrapping every attributed run in a LocalizedStringKey-backed Text.
At render time, SwiftUI's LocalizedStringKey.resolve walks the chain
and recurses ~8 stack frames per nesting level, overflowing the
main-thread stack for content that produces a few hundred runs
(e.g. emoji-dense notes where each emoji forms its own run).

Switch to Text + Text concatenation, which produces concat storage
that resolves linearly.

Fixes gonzalezreal#56
@bisonbet

Copy link
Copy Markdown
Owner Author

@claude can you review this PR?

@bisonbet

Copy link
Copy Markdown
Owner Author

@codex please review this PR - it says it can't be merged automatically, so we need to check why and what to do as well as the PR itself

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a996e51850

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// reduce step; resolving the resulting N-deep chain recurses ~8 stack frames per
// level during SwiftUI's render pass and overflows the main-thread stack for
// content that produces a few hundred attributed runs (e.g. emoji-dense notes).
self = textValues.reduce(Text(verbatim: ""), +)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the balanced TextBuilder on merge

When this PR is merged into current main, this line conflicts with the existing TextBuilder rewrite there; resolving the conflict by keeping this reduce(..., +) path would drop the plain-run coalescing and balanced concatenation that main already uses. For inputs with thousands of attributed runs, such as large syntax-highlighted code blocks or emoji-dense text, this still builds one left-deep Text concatenation node per run, so SwiftUI can recurse through thousands of ConcatenatedTextStorage nodes and hit the same stack-overflow class this PR is meant to fix. Resolve the conflict by keeping the current main implementation rather than replacing it with this left fold.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a996e51850

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// reduce step; resolving the resulting N-deep chain recurses ~8 stack frames per
// level during SwiftUI's render pass and overflows the main-thread stack for
// content that produces a few hundred attributed runs (e.g. emoji-dense notes).
self = textValues.reduce(Text(verbatim: ""), +)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the balanced TextBuilder on merge

When this PR is merged into current main, this line conflicts with the existing TextBuilder rewrite there; resolving the conflict by keeping this reduce(..., +) path would drop the plain-run coalescing and balanced concatenation that main already uses. For inputs with thousands of attributed runs, such as large syntax-highlighted code blocks or emoji-dense text, this still builds one left-deep Text concatenation node per run, so SwiftUI can recurse through thousands of ConcatenatedTextStorage nodes and hit the same stack-overflow class this PR is meant to fix. Resolve the conflict by keeping the current main implementation rather than replacing it with this left fold.

Useful? React with 👍 / 👎.

@bisonbet

Copy link
Copy Markdown
Owner Author

@codex Please review the comments you made on this PR and implement fixes

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a996e51850

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// reduce step; resolving the resulting N-deep chain recurses ~8 stack frames per
// level during SwiftUI's render pass and overflows the main-thread stack for
// content that produces a few hundred attributed runs (e.g. emoji-dense notes).
self = textValues.reduce(Text(verbatim: ""), +)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Balance concatenation to avoid another deep Text tree

For inputs that still produce many runs, such as a large syntax-highlighted code block built in HighlightedTextFragment.Model.highlight, this left fold constructs an N-deep Text + Text tree. That avoids the LocalizedStringKey recursion from the old interpolation path, but it still leaves SwiftUI with a deeply nested concatenation to resolve during layout, so the same stack-overflow class can recur when there are thousands of token/link/attachment runs. Build the concatenation as a balanced tree (or coalesce plain attributed runs before concatenating) so tree depth is bounded instead of proportional to the number of runs.

Useful? React with 👍 / 👎.

@bisonbet

Copy link
Copy Markdown
Owner Author

@codex Please review the comments you made on this PR and implement fixes

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • Implemented the requested follow-up fix in TextBuilder by replacing the left-fold concatenation with a balanced pairwise concatenation strategy, so the Text + Text tree depth is bounded and no longer grows linearly with run count.
  • Added a new helper balancedConcatenation(_:) that combines run Text values level-by-level, preserving behavior for empty input and reducing deep nesting risk for very large highlighted/emoji-dense content.
  • Kept the original rationale comments and expanded them to explicitly document why both interpolation recursion and left-fold concatenation depth are problematic.
  • Committed the change on the current branch as commit 736b079.

Testing

  • ⚠️ swift test (failed due to environment network restriction while fetching SwiftPM dependencies: GitHub clone CONNECT tunnel failed, response 403, not due to code compilation/test assertions).

View task →

@bisonbet

Copy link
Copy Markdown
Owner Author

@codex this PR can't merge - please review and fix the blocking issues

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • Replaced the left-fold Text + Text reduction with a balanced pairwise concatenation call, so the generated SwiftUI Text tree depth grows logarithmically instead of linearly with attributed run count.
  • Added balancedConcatenation(_:), which preserves the empty-text behavior and combines adjacent Text values level-by-level to avoid deep concatenation chains.
  • Committed the fix on the current branch as 89af8f3 and created a follow-up PR titled “Balance TextBuilder concatenation on stack-overflow fix.”

Testing

  • swift format lint Sources/Textual/Internal/TextFragment/TextBuilder.swift
  • git diff --check
  • ⚠️ swift test — blocked by environment GitHub access while SwiftPM fetched dependencies; dependency clones failed with CONNECT tunnel failed, response 403.

View task →

@bisonbet

bisonbet commented Jun 1, 2026

Copy link
Copy Markdown
Owner Author

Closing — superseded by commit 8b1e47a ("fix(textfragment): bound Text recursion depth in TextBuilder") which is already on main via #2.

The fix on main is strictly stronger than this PR's reduce(_, +) left-fold:

  • Coalesces consecutive plain runs into a single Text(_ attributedString:) so the Text-node count is bounded by attachments + links + 1 rather than the total run count.
  • Concatenates across attachment/link boundaries with a balanced pairwise +, producing an O(log N)-deep ConcatenatedTextStorage tree instead of O(N).

As the file header comment on main notes, a left-fold reduce would re-introduce the same stack-overflow class for content with thousands of attachments/links, so this PR's approach isn't sufficient as a long-term fix.

@bisonbet bisonbet closed this Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants