Record the text-width defect, and that the depth-4 seeds are fixed

The two seeds a wider scan found were one defect, older than the branch: a
text reported a width rounded to the step below the line it measured, so
the box that came back to it broke differently. Two tolerances were holding
that together and both are gone.
This commit is contained in:
iris-ai committed 2026-09-17 03:21:35 -04:00
1 parent f64d6a8d4d
commit faa4e98311
1 file changed
+66 -37
+66 -37
View File
@@ -10,21 +10,19 @@ stayed.
Canonical Iris `main` is **`ca2b4b2`** (#17, the headless rig). **#18
`split/18-position-chain`** is open in `/home/bob/repos/iris-pr18`, head
**`ffd79f3`**, pushed. It holds LAYOUT.md §2's position chain, `leftover`,
**`c8beca5`**, pushed. It holds LAYOUT.md §2's position chain, `leftover`,
the `Holds` retained-layout contract, region nodes, built-in alignment and
size rules, fixed-point layout, a box in pixels threaded down the draw, and
a report read as a fraction of the containing widget. No PR review was
present when checked on 2026-09-15.
**The branch has a warm/cold divergence nothing on it explains yet**, found
2026-09-17 by widening the fuzzers rather than by any change: seeds **1121**
and **1839** at **depth 4** fail `shuffle-swap-for-three` and
`shuffle-all-but-first` respectively, and they fail the same way on
`ea6dbae`, before either of the day's commits. Nothing in the routine
verification reaches them -- the fast oracle takes ten seeds, the shrinker
400 at depth 5 and the long oracle 1000 at depth 6, and these are past 400
at a depth neither long run uses. Chasing them is first in "Next". The scan
that found them, which is worth keeping as a pattern:
**Widen one fuzzer axis at a time, and record which.** Seeds **1121** and
**1839** at **depth 4** failed on `ea6dbae` and on every commit before it,
and nothing in the routine verification reached them: the fast oracle takes
ten seeds, the shrinker 400 at depth 5 and the long oracle 1000 at depth 6,
so a defect past seed 400 at depth 4 had nowhere to show. Both are fixed by
`4bd8607`. The scan that found them, worth running again after any change
to layout:
```rust
// tests/scan.rs, deleted once it had done its job
@@ -38,9 +36,10 @@ over_seeds((1..=2000).collect(), |seed| {
});
```
262 s for 2000 seeds at depth 4 over all fifteen cases. `Rng::new` is
`seed | 1`, so an even seed and the odd one above it are one tree: 1120 and
1121 are the same counterexample, as are 1838 and 1839.
261 s for 2000 seeds at depth 4 over all fifteen cases, and clean at
`4bd8607`. `Rng::new` is `seed | 1`, so an even seed and the odd one above
it are one tree: 1120 and 1121 are the same counterexample, as are 1838 and
1839.
**Two ideas outrank everything else in this document** (Bryan, 2026-09-17).
First, a changed tree lays out exactly as if it had been drawn that way from
@@ -142,6 +141,38 @@ divergence needs a reader drawing while a size dependency two levels under
it is dirty, which no hand-built tree has reproduced yet, and the seeds at
depth 4 above fail for some other reason.
### A text is handed back a box its own line fits in (landed, `4bd8607`)
A wrapping text reported the width it used through `Px::from_f32`, which
takes the nearest step and is under the line the shaper measured half the
time. A parent that sizes itself from that report -- a stack taking a span's
width, the span taking its widest child's -- then hands the text back a box
its own longest line does not fit in, and a greedy break there is one line
longer. Warm kept the break it had; cold made the narrower one.
Two tolerances were holding that together and both are gone:
- `TextBuffer::shape` answered any width within `BREAK_EPSILON_PX = 0.05` of
the longest line from the break in hand. Fifty steps of the grid, and a
structural decision taken on a hair's breadth -- the thing the `Span`
boundary invariant below already forbids. It is `want >= layout.width()`
now, exactly.
- The `Holds` range the text declares started at the nearest step to its
longest line, so it admitted boxes that line does not fit in. It starts at
`Px::ceil_from_f32` of it now.
Neither was the fix. **The fix is the report**: `Size::from_px(
PxVec2::ceil_from_f32(tex.size))`, the step at or above what was measured,
so the box that comes back fits. With it in place either tolerance could
have stayed and the case passes; both are wrong on their own terms, so both
went. `Fixed::ceil_from_f32` is new and is the only rounding on the grid
that is not to the nearest step.
The general shape, and the third time this branch has hit it: **a value that
comes back as a box has to be rounded away from the measurement, not to the
nearest step.** Rounding to nearest is right for a value being carried;
it is wrong for a bound.
### `Span`'s leftover boundary is a third expression for the room
The decision uses a rounded division, `total.px.div(fixed)`, while the room
@@ -446,19 +477,24 @@ above for what closing the rest would cost.
## Verification at the current head
At `ffd79f3`:
At `c8beca5`:
- `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --
-D warnings`, `cargo test --workspace`: green, 89 suite tests, 18 core
-D warnings`, `cargo test --workspace`: green, 90 suite tests, 19 core
unit tests, 11 generated cases. Only the long runs and the profiling rigs
are ignored; no known defect is.
- The release oracle at 100 seeds in 14.3 s, and **120 seeds in debug** in
59 s -- the debug run exercises the `Holds` assertion in `draw_at`.
- All fifteen shrinker cases at 400 seeds of depth 5 in 56 s, and the
oracle at 1000 seeds of depth 6 in 142 s.
- `view`, `minimal`, `random`, `tabs` and `text` byte-identical at
1920x1200 against `ea6dbae`. `random` live-resized from 1920x1200 to
1280x800 is byte-identical to a cold 1280x800 render.
- All fifteen shrinker cases at 400 seeds of depth 5 in 56 s, the oracle at
1000 seeds of depth 6 in 142 s, and **2000 seeds at depth 4 over all
fifteen cases** in 261 s.
- `view`, `minimal`, `random` and `tabs` byte-identical at 1920x1200
against `ea6dbae`. `random` live-resized from 1920x1200 to 1280x800 is
byte-identical to a cold 1280x800 render. **`text` is a new picture**:
`4bd8607` moved its lower paragraph one pixel, the box being a step wider
and its left edge crossing the shader's snap, and `c8beca5` rewrote the
alignment panel, which had all three labels in the middle of a box the
width of the widest of them.
- Twenty-five rig work counters identical on `cold`, `repaint`, `scroll`,
`resize` and `size` across `0e0d4af`; `many` differs only in reuse
attempts.
@@ -632,22 +668,15 @@ The replay used for the reference check:
In order, from the review above and Bryan's steer (2026-09-17):
1. **Seeds 1121 and 1839 at depth 4**, which fail on this head and on
`ea6dbae` alike -- a warm/cold divergence older than either of the
day's commits and the only known correctness defect on the branch.
Reduce each with `SHRINK_SEED`/`SHRINK_DEPTH=4`/`SHRINK_CASE`, turn it
into a fast test, and add the seed to `generated.rs`'s `SEEDS` the way
86 and 20 are there. Then re-scan 2000 seeds at depth 4 before calling
it done.
2. **A fast test for `0e0d4af`**, the stale-answer guard, which went in
1. **A fast test for `0e0d4af`**, the stale-answer guard, which went in
with only a fuzz seed behind it. The shape wanted is a widget drawing
while a size dependency two levels under it is dirty, where its
drawing is reusable at the measuring box and not at the placing one.
3. Write `ActiveData::answer` in one place.
4. Keep the `DrawInfo` on `ActiveData`; delete the copied fields and the
2. Write `ActiveData::answer` in one place.
3. Keep the `DrawInfo` on `ActiveData`; delete the copied fields and the
reconstruction in `redraw`.
5. `Span`'s leftover boundary through `Holds::through`.
6. **Round on the CPU and snap to the nearest pixel in the shader**, as
4. `Span`'s leftover boundary through `Holds::through`.
5. **Round on the CPU and snap to the nearest pixel in the shader**, as
one change with one verification. Bryan approved the snap on 2026-09-17
(rendering may change wherever it brings the screen closer to what the
user's code says: three equal sections of 1000 px need one of them
@@ -663,20 +692,20 @@ In order, from the review above and Bryan's steer (2026-09-17):
bounds move by half a `Rel` step); check with `nm` that `UiSpan::within`
still inlines; expect a couple of percent of instructions and re-run the
long fuzzers and the render set once for both.
7. The smaller items: the stale `f32` comment, the gap of an undrawn child,
6. The smaller items: the stale `f32` comment, the gap of an undrawn child,
confirm nested `leftover` weights, one zero-divisor fallback. Add to
them: a span that overflows itself hands a child a box of negative
length, which is ordinary now rather than a corner, and nothing states
what a widget may assume about one.
8. `LazySpan`, the next LAYOUT.md §2 item. Region nodes cover the movable
7. `LazySpan`, the next LAYOUT.md §2 item. Region nodes cover the movable
subtree case; do not restore a separate child-placement API.
9. `SizeRule::{Min, Max, Clamp}`, restoring the `max_width`/`max_height`
8. `SizeRule::{Min, Max, Clamp}`, restoring the `max_width`/`max_height`
builders `8220a78` deleted. The clamp boundary is a hard layout decision
with an exact `Holds` split at the crossover, both sides in `Px`. Still
awaiting Bryan: whether a `Max` narrows the box the child draws in, or
only what the parent reports for it.
10. `Scroll` taking a direction rather than one axis.
11. The measure/draw split, once the above is in. It deletes the two-ask
9. `Scroll` taking a direction rather than one axis.
10. The measure/draw split, once the above is in. It deletes the two-ask
protocol, which is what `0e0d4af` had to put a guard around.
`docs/LAYOUT.md` §4, §5 and the density section are stale: they name