Grow padding into the random trees, and take children in and out of spans

Padding as a node, with each of the four sides its own number: a padding that
is the same all round hides anything that treats one edge differently from
another. Spans now hold two to four children, so a pattern of removals has
something to make a pattern out of.

Five ways of changing what a span holds, each a shape worth its own case
rather than one shuffle: every other child out, everything but the first out,
three on at once, the first out and three on, and one out of the middle with
one on the end. Each is applied to every third span, and the cold tree is
grown holding exactly what the warm one was left with.

Three spare leaves are grown beside every span whether they end up in it or
not, so a tree that leaves them out makes the same widgets in the same order
as one that puts them in -- otherwise the two trees' `ids` stop lining up at
the first difference and every comparison after it is against the wrong
widget. Attaching one moves it, since a widget belongs to one parent;
`upgrade` is for a weak handle that was never added, not a second share. The
detached children are held until the comparison is over for the same reason:
dropping the last share of one frees its id for the next widget to be given.

Each case asserts the tree actually changed before comparing, so a shuffle
that quietly did nothing fails rather than passes.

All of it agrees: 49 tests, and the ignored sweep over 100 seeds and eight
scenarios, 800 comparisons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-14 13:48:42 -04:00
1 parent 2272634dc5
commit 4178dfbff9
3 files changed
+237 -31

No files matched your search

+89 -19
View File
@@ -11,6 +11,24 @@ use std::collections::HashMap;
/// The declared lengths of one `SetSize`, by axis.
pub type Lens = [Option<Len>; 2];
/// What a test changes between two trees grown from the same seed, so the
/// warm one can be mutated and the cold one grown that way to begin with.
#[derive(Default)]
pub struct Edits {
/// Declared sizes, by the order the `SetSize` wrappers were made.
pub sizes: HashMap<usize, Lens>,
/// Which children a span has, by the order the spans were made.
pub spans: HashMap<usize, SpanEdit>,
}
#[derive(Default, Clone)]
pub struct SpanEdit {
/// Children to leave out, by index among the ones grown.
pub detach: Vec<usize>,
/// How many of the span's spares are in it, appended in order.
pub attach: usize,
}
/// xorshift64, written out rather than taken from a crate so that a seed
/// keeps meaning the same tree.
pub struct Rng(u64);
@@ -45,6 +63,9 @@ const COLORS: [UiColor; 6] = [
UiColor::MAGENTA,
];
/// Leaves grown beside every span, for a test to put into it.
const SPARES: usize = 3;
const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer and not a setting.";
@@ -55,6 +76,22 @@ const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
pub struct Tree {
pub ids: Vec<WidgetId>,
pub sized: Vec<WeakWidget<SetSize>>,
pub spans: Vec<Spanned>,
/// Children a `SpanEdit` took out, held so that dropping the last share
/// of one does not free its id for the next widget to be given -- which
/// would put the two trees' `ids` out of step.
pub detached: Vec<StrongWidget>,
}
pub struct Spanned {
pub id: WeakWidget<Span>,
/// Leaves grown with the span whether or not they end up in it, so both
/// trees make the same widgets in the same order either way. Attaching
/// one moves it out of here: a widget belongs to one parent, and one that
/// belongs to nobody still has to be held or it reads as a leak.
pub spares: Vec<StrongWidget>,
/// How many children it was grown with, before any edit.
pub grown: usize,
}
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
@@ -63,7 +100,7 @@ pub fn grow<Rsc: UiRsc + 'static>(
rsc: &mut Rsc,
seed: u64,
depth: usize,
edits: &HashMap<usize, Lens>,
edits: &Edits,
) -> (StrongWidget, Tree) {
let mut grow = Grow {
rsc,
@@ -79,7 +116,7 @@ struct Grow<'a, Rsc> {
rsc: &'a mut Rsc,
rng: Rng,
tree: Tree,
edits: &'a HashMap<usize, Lens>,
edits: &'a Edits,
}
impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
@@ -117,7 +154,7 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
}
let idx = self.tree.sized.len();
let lens = [self.len(), self.len()];
let lens = self.edits.get(&idx).copied().unwrap_or(lens);
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
let id = SetSize {
inner,
x: lens[0],
@@ -133,29 +170,62 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
if depth == 0 {
return self.leaf();
}
let count = 2 + self.rng.below(2);
let mut children = Vec::with_capacity(count);
for _ in 0..count {
if self.rng.below(4) == 0 {
let inner = self.node(depth - 1);
let inner = self.sized(inner);
// Each side its own, since a padding that is the same all round
// hides anything that treats one edge differently from another.
let mut side = || self.rng.below(24) as f32;
let padding = Padding {
left: side(),
right: side(),
top: side(),
bottom: side(),
};
let id = Pad { padding, inner }.add_strong(self.rsc);
self.tree.ids.push(id.id());
return id;
}
let grown = 2 + self.rng.below(3);
let mut children = Vec::with_capacity(grown);
for _ in 0..grown {
let child = self.node(depth - 1);
children.push(self.sized(child));
}
let id: StrongWidget = match self.rng.below(3) {
0 => Stack {
if self.rng.chance() {
let id = Stack {
children,
size: StackSize::Child(0),
}
.add_strong(self.rsc),
_ => {
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
Span {
children,
dir,
gap: self.rng.below(3) as f32 * 4.0,
}
.add_strong(self.rsc)
.add_strong(self.rsc);
self.tree.ids.push(id.id());
return id;
}
// Grown either way, so the widget after them has the same id in a
// tree that leaves them out as in one that puts them in.
let mut spares: Vec<StrongWidget> = (0..SPARES).map(|_| self.leaf()).collect();
let idx = self.tree.spans.len();
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
// Highest first, so an index means the same child however many of its
// neighbours are going too.
let mut detach = edit.detach.clone();
detach.sort_unstable();
for j in detach.into_iter().rev() {
if j < children.len() {
self.tree.detached.push(children.remove(j));
}
};
}
let attach = edit.attach.min(spares.len());
children.extend(spares.drain(..attach));
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
let id = Span {
children,
dir,
gap: self.rng.below(3) as f32 * 4.0,
}
.add(self.rsc);
self.tree.ids.push(id.id());
id
self.tree.spans.push(Spanned { id, spares, grown });
id.add_strong(self.rsc)
}
}