A ninth sweep, over the part no earlier round named -- the widget vocabulary and the builder methods, `Widgets`, the examples, the `util` additions and the manifests -- and once more over `77ed7a2`, the eighth sweep's own commit and so itself unreviewed. A hint overrode a rule. `declared_lens` asked `rules[axis].declared()` first and fell through to the widget's own `size_hint` whenever that answered `None` -- which it does for a share, since a share is not a declaration. So a widget carrying `width(leftover(1))` and hinting a pixel length of its own was handed a box of the hint, against the rule and against the comment inside the function: "a hint still narrows the box where no rule does". `Painter::size_hint` spells the same rule-else-hint step three hundred lines up and gets it right, with the reason written on it; both read `Widgets::exact_len` now, and `declared_lens` is the part of its answer that needs nobody to divide it. `Image` is the only widget here whose hint is a declared length, and neither the tests nor the generator builds one, so nothing in this repository could reach the difference -- which is why the dump is unchanged and why the test builds a widget of its own. It records the box it was asked in: 400 with the rule and 50 without, and 50 either way before this. Marking a widget for redraw had no name. Twenty-one sites under `tests/` said it as `widgets_mut().get_dyn_mut(id);` with the widget thrown away, five with a `let _ =` in front, one with a comment explaining what the line was for, and one wrapped in a local function called `mark`. `Widgets::mark_for_redraw` says it. `revision_cost.rs` keeps the long spelling and now says why in place: it is deliberately in the API subset an old worktree also has. `assert_same_regions` could not see the defect the eighth sweep had just fixed. It zips the warm and cold id lists, so a list naming one widget twice -- which is what `width`, `sized` and `align` giving back their own argument produces -- compares fewer boxes than it lists and says nothing about it. It now rejects a repeated id and two lists of different lengths, which also checks the nine fixtures that round left alone: all eighteen cases pass. Bare pairs where the framework has named ones. `random.rs`'s `Lens` and `Aligns` were `[Option<LayoutLen>; 2]` and `[Option<AxisAlign>; 2]`, read as `[0]`/`[1]` and zipped against a hand-written `[Axis::X, Axis::Y]`. They are `SizeRules` and `Align`; `Align` took the `Index<Axis>` every other per-axis pair on this branch has, and `RegionAlign::from` does the "an axis left out is centred" step two rigs were spelling per axis. The three sites that wrote the axis pair out say `Axis::BOTH`, which is what the rest of the layout code says. `BothAxis<T>`, `AxisT`, `XAxis` and `YAxis` -- 45 lines with a const trait, two marker types and three accessors -- have no user anywhere in the workspace. They are the mechanism `impl_axis_index!` replaced, in the file this branch took `Vec2::axis`/`axis_mut` out of. Deleted, which is a drive-by in a block the branch was already rewriting; drop it if the scope matters more. Smaller things, each in its own place: `Wrapper` arrived beside core's `WidgetWrapper`, one word for a widget that wraps a child and for a dynamic borrow guard, so the alias is gone and its two uses name `DynBorrower` -- which is what they are. `Wrapper::new`, `Wrapper::empty` and its `Default` were three names for one value, two of them unused. `Arena::get_mut` was the only `pub(crate)` among `pub` siblings on a public type. `Selector` rounded the pointer onto the pixel grid to do arithmetic on two values already there, losing the precision the platform gave it for nothing; the step between the regions is taken on the grid instead. And the two `debug` profile settings carry their reason where the next reader looks rather than only in the commit that made them, one of which was about renaming `rest`. Format, clippy with and without layout-diagnostics, and the suite (132 + 19 + 13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to `77ed7a2` across all 34,488 boxes, and all three seed scans pass: 400 at depth 5 in 63.27s, 1,000 at depth 6 in 160.45s, 2,000 at depth 4 in 302.52s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
140 lines
4.5 KiB
Rust
140 lines
4.5 KiB
Rust
//! The tree a seed describes, as a value rather than as widgets.
|
|
//!
|
|
//! Two things have to hold for a plan to be worth having. Editing a plan has
|
|
//! to mean what growing with those edits means, or a scenario reads one thing
|
|
//! and the oracle another. And reducing a plan has to end, or a shrinker
|
|
//! searching for the smallest counterexample never returns.
|
|
|
|
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan};
|
|
use std::collections::HashMap;
|
|
|
|
fn some_edits(seed: u64, of: &Plan) -> Edits {
|
|
let mut rng = Rng::new(seed);
|
|
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
|
|
let mut of = of.clone();
|
|
of.walk_mut(&mut |p| {
|
|
if matches!(p.kind, Kind::Span { .. }) {
|
|
spans += 1;
|
|
}
|
|
sized += p.size.is_some() as usize;
|
|
aligned += p.align.is_some() as usize;
|
|
nodes += p.region_node.is_some() as usize;
|
|
});
|
|
let pick =
|
|
|n: usize, rng: &mut Rng| -> Vec<usize> { (0..n).filter(|_| rng.chance()).collect() };
|
|
Edits {
|
|
sizes: pick(sized, &mut rng)
|
|
.into_iter()
|
|
.map(|i| {
|
|
(
|
|
i,
|
|
SizeRules {
|
|
x: SizeRule::Exact(LayoutLen::LEFTOVER),
|
|
y: SizeRule::Free,
|
|
},
|
|
)
|
|
})
|
|
.collect(),
|
|
aligns: pick(aligned, &mut rng)
|
|
.into_iter()
|
|
.map(|i| {
|
|
(
|
|
i,
|
|
Align {
|
|
x: Some(AxisAlign::POS),
|
|
y: None,
|
|
},
|
|
)
|
|
})
|
|
.collect(),
|
|
nodes: pick(nodes, &mut rng)
|
|
.into_iter()
|
|
.map(|i| (i, true))
|
|
.collect(),
|
|
spans: pick(spans, &mut rng)
|
|
.into_iter()
|
|
.map(|i| {
|
|
(
|
|
i,
|
|
SpanEdit {
|
|
detach: vec![0],
|
|
attach: 2,
|
|
},
|
|
)
|
|
})
|
|
.collect::<HashMap<_, _>>(),
|
|
fixed_branches: false,
|
|
}
|
|
}
|
|
|
|
use iris::prelude::*;
|
|
|
|
/// The two routes to an edited tree are one tree. `plan` resolves edits out
|
|
/// of the random stream as it draws; `edited` puts them on a tree that
|
|
/// already exists, which is the only route a shrunk plan has, since no seed
|
|
/// grows one. A scenario written against either has to read the same.
|
|
#[test]
|
|
fn editing_a_plan_is_growing_one_with_those_edits() {
|
|
for seed in 1..=60 {
|
|
let bare = plan(seed, 5, &Edits::default());
|
|
let edits = some_edits(seed, &bare);
|
|
assert_eq!(
|
|
bare.edited(&edits),
|
|
plan(seed, 5, &edits),
|
|
"seed {seed}: edited and grown-with-edits disagree"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// No simplification is larger, which is the half of "the shrinker stops" a
|
|
/// widget count can see. Most are not smaller either -- a dropped alignment
|
|
/// and a simpler leaf both keep the count -- so what rules out circling is
|
|
/// that those are one-way too: a `Some` becomes a `None`, and a kind steps
|
|
/// down a ladder with no way back up.
|
|
#[test]
|
|
fn no_simplification_of_a_plan_is_larger_than_it() {
|
|
for seed in 1..=60 {
|
|
let tree = plan(seed, 4, &Edits::default());
|
|
let mut queue = vec![tree];
|
|
let mut seen = 0;
|
|
while let Some(node) = queue.pop() {
|
|
seen += 1;
|
|
if seen > 400 {
|
|
break;
|
|
}
|
|
for small in node.smaller() {
|
|
assert!(
|
|
small.size() <= node.size(),
|
|
"seed {seed}: a simplification grew from {} to {}",
|
|
node.size(),
|
|
small.size()
|
|
);
|
|
if small.size() < node.size() {
|
|
queue.push(small);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reducing until nothing reduces ends, and ends at something small enough to
|
|
/// read rather than at the tree it started from.
|
|
#[test]
|
|
fn reducing_a_plan_all_the_way_ends() {
|
|
for seed in 1..=30 {
|
|
let mut node = plan(seed, 5, &Edits::default());
|
|
let grown = node.size();
|
|
let mut steps = 0;
|
|
while let Some(next) = node.smaller().into_iter().next() {
|
|
node = next;
|
|
steps += 1;
|
|
assert!(steps < 10_000, "seed {seed}: reducing did not end");
|
|
}
|
|
assert!(
|
|
node.size() < grown.max(2),
|
|
"seed {seed}: reduced {grown} widgets to {}",
|
|
node.size()
|
|
);
|
|
}
|
|
}
|