Branch on a measurement, so a wrong one shows as a different tree

Comparing boxes catches a widget that moved. It does not catch one that
measured a child, was handed an answer a cold start would not have given,
and took the other branch -- the same defect, arriving where a pixel
comparison cannot see it. Branching on what the painter tells you is
something a widget is allowed to do, so the library owes the same answer
warm and cold; only a widget changing its own configuration is exempt.

`random::Branch` measures a child and draws one of two others on the
result, with both grown either way so the ids match whichever is drawn.
It joins the generator, which makes every existing scenario a control-flow
oracle as well as a geometric one. `tests/determinism.rs` is the same
widget by hand across eight thresholds, including either side of the
answer, and is the fast check -- the sweep is a fuzzer and confirms at the
end rather than being iterated against.

A span behind a branch nobody took is not drawn, so shuffling it cannot
move anything; `reshuffled` now treats that as vacuous, the way it already
treats a tree with no spans, rather than as a shuffle that had no effect.

Both new tests pass, and the sweep passes at depth 4 and 5 over 200 seeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-15 00:06:54 -04:00
1 parent 60175c3821
commit 1b1378b05a
3 files changed
+156 -2

No files matched your search

+46
View File
@@ -84,6 +84,35 @@ pub struct Tree {
pub detached: Vec<StrongWidget>,
}
/// Branches on a child's measured length. Comparing boxes catches a widget
/// that moved; this catches one that believed a measurement a cold start
/// would not have given it, by turning that into a different tree. Its own
/// configuration never changes, so which side draws is a property of the
/// layout alone.
pub struct Branch {
pub probe: StrongWidget,
pub wide: StrongWidget,
pub narrow: StrongWidget,
pub threshold: f32,
}
impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(40.0);
let measured = painter.place(&self.probe, top).len(Axis::X);
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
let mut rest = UiRegion::FULL;
rest.y.start = rest.y.start.offset(40.0);
match px > self.threshold {
true => painter.place(&self.wide, rest),
false => painter.place(&self.narrow, rest),
};
Size::REST
}
}
pub struct Spanned {
pub id: WeakWidget<Span>,
/// Leaves grown with the span whether or not they end up in it, so both
@@ -199,6 +228,23 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
if positioned == 2 {
// Both sides are grown either way, so a tree that draws one has
// the same ids as a tree that draws the other.
let probe = self.node(depth - 1);
let wide = self.node(depth - 1);
let narrow = self.node(depth - 1);
let threshold = self.rng.below(500) as f32;
let id = Branch {
probe,
wide,
narrow,
threshold,
}
.add(self.rsc);
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
if positioned == 1 {
let inner = self.node(depth - 1);
let inner = self.sized(inner);