Skip to content

Fuzz the fast renderer against the default one, and fix the KeyError it finds - #63

Merged
maartenbreddels merged 3 commits into
masterfrom
test/fuzz-fast-renderer
Sep 28, 2026
Merged

maartenbreddels merged 3 commits into
masterfrom
test/fuzz-fast-renderer

Conversation

@maartenbreddels

@maartenbreddels maartenbreddels commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Why

The fast renderer (REACTON_FAST=1) is tested by running the test suite with REACTON_FAST=1. Those tests were written for the default renderer, so they rarely hit the cases the fast renderer skips: an element that is the same object in the next render, in a subtree that did not change. Bugs in those skips went unnoticed.

What

Two commits:

  1. The fuzz test (reacton/fuzz_test.py, from perf: make fast-renderer updates 10-60x cheaper and fix a state memory leak #58). It renders random component trees that change shape with their state (container type flips, keys, shuffles, fragments, wrappers, effects that set state, caught exceptions) and sets random state. After every step it checks that both renderers give the same widgets and run the same effects in the same order. On master, seed 9 of the 12 fails with a KeyError in the fast renderer.
  2. The fix.
    • The bug: a child element that is the same object in the next render keeps its previous subtree. But when the widget that holds it is replaced by a widget of another type (VBox → HBox), reconciliation first removes the old subtree, including the child's component contexts. So the fast renderer crashes with a KeyError. The default renderer does not.
    • The fix: while the render phase walks the children of a widget that replaces a widget of another type, the fast path is off, so the child is rendered again. This is about 15 lines, taken from perf: make fast-renderer updates 10-60x cheaper and fix a state memory leak #58 without its "skip equal children" speed-up.

A child element is the same object in two renders when it comes from outside (a prop), or when it is the root element of a component that did not render again.

Tests

  • Fuzz test, 300 seeds with 25 steps each (a local run, the PR keeps 12): 62 fail on master, all with this KeyError. With the fix, all 300 pass.
  • test_replace_parent_same_child_element: the small case, which fails on master in the fast renderer.
  • pytest reacton/ with REACTON_FAST=0 and =1: all pass.
  • Solara's unit suite: the same results as master, in both renderers.

Review

Crossreview by three reviewers (astra, opus, glm): all three approve the fix.

  • Checked and correct:
    • The guard uses the same key and condition as reconciliation.
    • The counter is restored in finally.
    • Renders, effects and cleanups are the same as in the default renderer, including nested replacements, a component whose root changes type, a .shared() child, and a replacement in a second render pass.
  • Small fix, from the review (confirmed by the driver): the fuzz test now also checks for leaked widgets and callbacks (the cleanup_guard fixture only ran for core_test.py), and it compares whether widgets are closed.
  • Not fixed here, an older bug: a child with an explicit .key() that moves out of a container whose type changes, into a sibling container, in the same render, still gives this KeyError in the fast renderer. It fails the same way on master. The default renderer handles it. The fuzz test cannot find it, because its children never move between containers. This is a follow-up.

🤖 Generated with Claude Code

maartenbreddels and others added 3 commits September 28, 2026 15:47
The fast renderer was tested by running the test suite with
REACTON_FAST=1, but those tests were written for the default renderer
and rarely hit the cases the fast renderer skips: an element that is
the same object in the next render, in a subtree that did not change.

This test (from #58) renders random component trees
that change shape with their state, sets random state, and checks that
both renderers give the same widgets and run the same effects in the
same order after every step. Seed 9 fails on master: the fast renderer
raises a KeyError, fixed in the next commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In the fast renderer, a child element that is the same object in every
render (it comes from outside, e.g. a prop, or it is the root element
of a component that did not render again) keeps its previous subtree
when nothing in it changed. But when the widget that holds it is
replaced by a widget of another type (a VBox becomes an HBox),
reconciliation first removes the old subtree, including the child's
component contexts. Keeping the child as it was then made the render
fail with a KeyError. The default renderer does not have this problem.

The fuzz test from the previous commit finds it: on master, 62 of 300
seeds (25 steps each) fail with this KeyError; with this fix all pass.

While the render phase walks the children of a widget that replaces a
widget of another type, the fast path for unchanged children is now
off, so the child is rendered again.

Taken from #58 (part of "Skip a child component with
equal arguments without walking it"), without the skip itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review found two holes: the leak check (the autouse cleanup_guard
fixture) only ran for core_test.py, and the widget signature reads
traits that a closed widget still has, so a renderer that returned a
closed widget would match an open one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@maartenbreddels
maartenbreddels merged commit 84d4a13 into master Sep 28, 2026
24 checks passed
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.

1 participant