Bring the handoff up to date with what the fuzzer found
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1ffe6ea067
commit
98dcb31c23
1 file changed
+86
-32
@@ -509,42 +509,96 @@ would catch anything where keeping a drawing is what goes wrong, which is what
|
||||
weaker instrument, because building a second tree is not the same code path as
|
||||
refusing to reuse the first.
|
||||
|
||||
### A span's size across its own axis
|
||||
### Why a warm frame and a cold one disagree
|
||||
|
||||
`Span` reported `max(children abs)` across its axis, except that one child with
|
||||
any `rel` or `rest` flipped the whole span to `Len::REST` -- a discontinuity
|
||||
its own `TODO` admitted. So any disagreement about a child's reported size
|
||||
moved the span between tight and filled rather than by a little, and every
|
||||
child is placed `FULL` across the axis, so they all inherit the move together.
|
||||
That is what the depth-5 seed-10 divergence looks like: three texts, identical
|
||||
heights, all jumping from 874 px wide to 306.
|
||||
Three defects, found on 2026-09-15 by shrinking a fuzzer's counterexamples.
|
||||
Two are fixed. The third is the one the others were hiding.
|
||||
|
||||
`OrthoSize::{Fill, Children}` replaces it, chosen per span rather than inferred
|
||||
from what the children happen to report. `Children` propagates the largest
|
||||
child whole -- a child at `rel` 0.5 makes the span `rel` 0.5 -- and compares
|
||||
candidates in pixels only when one has a share the other does not, since that
|
||||
is the only case the components cannot be compared directly. Reading pixels
|
||||
costs `OnResize::Scale` across the axis, which is why the choice is explicit.
|
||||
**A first frame was wrong, and nothing about retained state was involved.**
|
||||
`SetSize` drew its child in whatever box it had been offered and then reported
|
||||
its *declared* length, so the child answered about a box it was never going to
|
||||
have -- and the answer on the other axis was taken under that. A wrapping text
|
||||
under `SetSize(x: 76px)` was measured in the whole 640 available, said one
|
||||
line, and the parent sized itself to one line; the text was drawn again at 76
|
||||
and said two, too late. It now measures in the length it declares. Six widgets
|
||||
reproduce it in `tests/unsettled.rs`.
|
||||
|
||||
**It is circular, and this is the open question.** `Children` reads the span's
|
||||
own box to decide which child is longest, and what it reports decides that box:
|
||||
offered 900 it may report 306, be given 306, and on the next draw compare
|
||||
against 306 and pick a different child. `adding_and_removing_span_children`
|
||||
seed 13 diverges in release with 112 widgets wrong, at `DEPTH = 4`. The
|
||||
comparison wants a reference that does not depend on the answer -- the offered
|
||||
box rather than the settled one -- which is stable until a `Children` span
|
||||
nests inside another. This is the cross-axis twin of the along-axis
|
||||
double-shaping in LAYOUT.md §4, not a separate defect.
|
||||
That one matters beyond itself: **`generated.rs` compares a warm frame against
|
||||
a cold one, and cold was not a fixed point either.** Some of what this document
|
||||
previously called a retained-layout defect was the cold side being wrong.
|
||||
|
||||
**Depth 4 is not enough.** `tests/generated.rs` ran at `DEPTH = 4` and the
|
||||
generator branches two to four ways per level, so depth is exponential in width
|
||||
and a deep tree cannot be reached by raising it. `IRIS_GENERATED_DEPTH` and
|
||||
`IRIS_GENERATED_SEEDS` now select the load. At depth 5 the first 300 seeds
|
||||
already fail -- seed 10, `AddThree`, a wrapping text under a span whose axis is
|
||||
**Y**, which the comment on `a_long_run_of_seeds_agrees` claims is the stable
|
||||
case. That claim is wrong, and the "7 of these 90 / 30 with it" numbers beside
|
||||
it do not match a sweep that passes clean at depth 4; re-measure before
|
||||
trusting them.
|
||||
**Re-breaking a text at its own longest line is a knife edge.** A parent that
|
||||
sizes to a child offers back the length the child just reported, composed back
|
||||
through the box chain -- so it lands an ulp either side, and which side decides
|
||||
whether the longest line still fits. Three lines at 167.41 or four at 163.49,
|
||||
from the same text in the same box. A greedy break does not need recomputing
|
||||
there: breaking at one width gives lines that each fit and none of which could
|
||||
have taken another word, so at any width down to the longest of them the same
|
||||
break holds. `TextBuffer::shape` answers across that interval, with a
|
||||
sub-pixel tolerance for the edge.
|
||||
|
||||
**What is left is not a stale drawing -- the layout has two answers.** A span
|
||||
measures its children in its own box, and its own box is what its parent gave
|
||||
it, from the size it reported, from those children. Four widgets:
|
||||
|
||||
Aligned(mid, -, Span[ Text(wrap), OneLine ])
|
||||
|
||||
Swap the two children of a live tree and the span is still 663.376 wide, so the
|
||||
text is offered 357.44, which is what it already holds; the size is valid, the
|
||||
span reports 663.376 again, nothing moves. Grow the same tree in that order and
|
||||
the span is offered the window, the text is asked for 334.06 and answers
|
||||
318.45, and the span settles at 624.38. **Both are stable.** Which one you get
|
||||
depends on what the tree was before.
|
||||
|
||||
So no rule about when to keep a drawing can fix it, and several were tried.
|
||||
`Painter::settle` -- place a child into its own reported size without measuring
|
||||
it there -- passes all four cases in `tests/unsettled.rs` and fails two in
|
||||
`generated.rs`, in `Aligned` alone, in `Span` alone, with the child force-drawn
|
||||
first or not. The fix wants the constraint a container measures under to be
|
||||
something it is *given* rather than something it *ends up with*.
|
||||
|
||||
`OrthoSize::{Fill, Children}` -- a span choosing whether to fill across its
|
||||
axis or report its longest child -- is written and parked for the same reason:
|
||||
`Children` reads the span's own box to rank candidates, which is this cycle
|
||||
with a second face. It is not the cause of anything, and the divergence it was
|
||||
blamed for reproduces without it.
|
||||
|
||||
### Shrinking a counterexample
|
||||
|
||||
`tests/generated.rs` is a fuzzer, and a seed is not a lead anybody can read:
|
||||
reconstructing one of its failures by hand failed three times. Two things fixed
|
||||
that, and both are worth keeping.
|
||||
|
||||
`describe` prints what each ancestor of a mismatch was *configured* with rather
|
||||
than its type name, so a run says `Text < SetSize{x:34 px;} < Aligned{x:neg,
|
||||
y:pos}` and the tree can be written out again. `Widget: Any`, so it needs no
|
||||
plumbing.
|
||||
|
||||
`tests/shrink.rs` grows trees from a description it can simplify -- drop a
|
||||
child, unwrap a wrapper, shorten a text, drop a declared length, reorder a
|
||||
span -- and takes the first simplification that still fails until none does. It
|
||||
reduced 402 widgets to 6, 905 to 6, and 486 to 4. It lives in the tests and the
|
||||
library knows nothing about it. Validate it after changing it: with the
|
||||
box-length check in `try_reuse` deliberately disabled it should reduce a
|
||||
96-widget tree to 2, and an early version of it silently found nothing because
|
||||
it never framed before resizing, so "warm" had no retained state at all.
|
||||
|
||||
Run the ordinary tests first, then the fuzzers, and turn what they find into a
|
||||
fast test rather than leaving a seed as the record.
|
||||
|
||||
**Depth 4 was hiding all of this.** `tests/generated.rs` ran at a constant
|
||||
`DEPTH = 4`; `IRIS_GENERATED_DEPTH` and `IRIS_GENERATED_SEEDS` now select the
|
||||
load, and `SHRINK_DEPTH`, `SHRINK_SEEDS` and `SHRINK_CASE` do the same for the
|
||||
shrinking fuzzer. The generator widens two to four ways per level, so depth
|
||||
buys overlap between dependency paths rather than ancestry. Nothing yet covers
|
||||
a deep narrow chain, which is a gap: Iris asked for high layer counts on
|
||||
2026-09-14 and a hundred-deep tree is still unreachable by turning this knob.
|
||||
|
||||
The comment on `a_long_run_of_seeds_agrees` blames a wrapping text on a span's
|
||||
own axis and says such a text is stable on any other axis. Both halves are
|
||||
wrong -- a divergence was found on a span whose axis is Y -- and the "7 of
|
||||
these 90 / 30 with it" numbers beside it do not match a sweep that passes
|
||||
clean. Re-measure before trusting them.
|
||||
|
||||
### Dirtying many widgets at once
|
||||
|
||||
|
||||
Reference in new issue
Block a user