diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 514d9f4..d23e0e1 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -4,10 +4,42 @@ Where the work in flight stands. The settled layout design, the vocabulary and the measurement method are in `docs/LAYOUT.md`; what the review of #19 found is in `docs/LAYOUT_LOG.md`. +## Built-in bounds replace `MaxSize` on PR #19 + +Bryan clarified that the built-in attribute should replace the wrapper. +Work is in `/home/bob/repos/iris-deferred`, on `layout/one-ask`, at +`2ac0843` (pushed). Bounds now constrain the offer in `Placing::ask`; declarations +are retained as resolved window lengths, so movement does not decide the +bound again. `.max_width`/`.max_height` now edit the same widget, like the +minimum helpers, and `MaxSize` is removed. Preferred lengths and bounds +are independent, so either order of `.width` and `.max_width` works. The +app pin is unchanged. + +The review fixed three cases: `window_holds` erasing an ask's bound crossing +(seed 104, depth 5), inactive bound edits failing to update the parent's offer +dependencies (seed 144, depth 6, reduced further to three widgets), and a +fractional intrinsic cap being read against its already-allocated slot. +The latter has an absolute 75-in-300 geometry check; warm/cold agreement +alone cannot catch it. + +207 ordinary and 211 diagnostic tests pass, with warning-clean Clippy both +ways. All three layout scans pass (400 depth 5, 1,000 depth 6, 2,000 depth 4), +as do 400 depth-5 trees in each of three deferred-request corpora. All 34,986 +unbounded cold boxes match `4cb6f68`; bounded offers intentionally change. +The final unbounded-path optimization preserves every bounded cold box too. + +Five-run release instruction medians show capped rows using 21.77–29.64% +less CPU work than the wrapper. Plain/fixed-width retained resizes differ by +under 0.05%; forced redraws improve about 0.3%. Text workloads differ by at +most 0.13%. Independent preferences and bounds cost 32 extra inline bytes per +widget; all three allocation fixtures remain allocation-free after warm-up. +`docs/LAYOUT.md` records the contract and measurements. Artifacts are in +`/tmp/attribute-bounds/`. + ## A quality sweep of the deferred system is submitted on PR #19 -`layout/one-ask` is at `4cb6f68` in `/home/bob/repos/iris-deferred`. The tenth -sweep is the first review of `76aaf06` through `0e838e9` -- the bound rules, +The tenth sweep landed at `4cb6f68` in `/home/bob/repos/iris-deferred`. It is +the first review of `76aaf06` through `0e838e9` -- the bound rules, the deferred request path and the invalidation fix. Ten findings, the largest two being a bound's held length looked up a second time through a value that could not promise the end it named (`Bound::at` `expect`ed it), and a bound in @@ -176,7 +208,9 @@ repeated 34,492, which is a `wc -l` of the whole run rather than of its box lines, so count the lines that are a box (`grep -cE '^[0-9]+ [0-9]+ '`). It is the only thing that catches two same-typed values being swapped, which is the failure mode of a rename or a move. The repair moved 650 of those boxes, all from the collapsed-share -correction; every commit since has been byte-identical to `84dad21`. +correction. Subsequent review-only changes preserved cold geometry through +`4cb6f68`; the offered-bound change in `2ac0843` intentionally changes bounded +geometry while preserving all unbounded boxes. **Only when the change can alter what layout computes**: the three seed scans -- 400 at depth 5, 1,000 at depth 6, 2,000 at depth 4. They cost about a @@ -257,7 +291,8 @@ glyphs do not follow a shortened entry. (`SizeRule::{Min, Max, Clamp}`); a widget holds the box (`MaxSize`, which `.max_width`/`.max_height` build). Bryan settled the split on 2026-09-20 after four readings were measured; the reasoning is in `docs/LAYOUT_LOG.md`. - The deferred-request work fixes stale relative-bound reuse by tracking the + Bryan superseded that split with the built-in bounds work above. The + deferred-request work fixes stale relative-bound reuse by tracking the input base and comparing resolved bounds, with a reduced natural-size-hint regression and 400 depth-5 relative-bound trees. Intrinsic bounds remain `Len`s; comparisons involving leftover use `SizeRequest`. Ordinary generated diff --git a/docs/LAYOUT.md b/docs/LAYOUT.md index 4ff66c6..4b9e34a 100644 --- a/docs/LAYOUT.md +++ b/docs/LAYOUT.md @@ -12,6 +12,57 @@ in flight stands. The sections from "Three names, and the one argument that says them" onwards are the settled design, the findings that outlived the working log, and the measurement method. +## Bounds on the offered box + +`SizeRule` carries an optional preferred request and independent min/max bounds. +The bounds constrain both the box offered to a widget and its reported size. +`.min_width`, `.max_width` and their height variants edit that widget's rules +and preserve its identity and type; `MaxSize` is removed. Setting `.width` or `.height` preserves the bounds; setting a bound +preserves the preferred request and the other bound, in either call order. +`Widgets::set_len` makes the same edit at runtime; `set_size_rule` replaces +the complete axis. This replaces the rule/widget split recorded in +`LAYOUT_LOG.md` (Bryan, 2026-09-20). + +The shared ask resolves declarations and bounds against the incoming rel +base, compares the offer, and retains the chosen box length in `Declared`. +These are window lengths. Placement aligns a retained length in its destination +without resolving it again or comparing the destination against the bound. +The comparison's window range and incoming-region dependency invalidate the +ask when its inputs change. Ask constraints are combined after the draw, so a +widget widening its own `window_holds` cannot erase a bound's crossing. +Fractional rules also track their incoming base. Allocated slots use the +allocator's base for bounds; applying a 25% cap to its own 25% slot would +shrink it twice. Changing an inactive bound also reaches the parent, because +it changes which offers the parent can safely reuse. +A cap constrains wrapping and scroll viewports, but short intrinsic content +can still report less than the cap. Shares remain deferred requests for the +parent to allocate; the offered box does not replace their reported weight. + +Measured against `4cb6f68`, medians of five release-process instruction counts: + +| Fixture | Before | After | Change | +| --- | ---: | ---: | ---: | +| 128 capped rows, 2,000 resizes | 7.387B | 5.779B | -21.77% | +| Same, every widget redrawn | 13.864B | 9.755B | -29.64% | +| 128 plain rows, every widget redrawn | 10.041B | 10.010B | -0.31% | +| 128 fixed-width rows, every widget redrawn | 10.085B | 10.056B | -0.29% | +| 40 text rows, 1,000 resizes | 9.460B | 9.472B | +0.13% | + +Plain and fixed-width retained resizes differ by under 0.05%; text repaint, +edit and scroll differ by -0.05%, +0.07% and +0.06%. `tests/bounds_cost.rs` +asserts the same geometry for the old builders and new attributes, with 513 +active widgets before and 385 after in the capped fixture. The text runs use +`tests/revision_cost.rs`. These include cold setup and do not measure GPU or +phone frame times. The unbounded cold dump remains identical at 34,986 boxes; +bounded geometry changes intentionally because the offer is now constrained. + +The storage tradeoff is explicit: a `SizeRule` is now 40 bytes rather than 24, +so independent preferences and bounds add 32 inline bytes per widget across +both axes. Plain, expression-bounded and attribute-bounded fixtures each +perform zero allocations over 100 forced-redraw resize frames after warm-up. +Bounds introduce no heap allocation; an expression also loses +its former extra outer `Arc`. Artifacts are in `/tmp/attribute-bounds/`. + ## Deferred size requests on PR #19 `Widget::draw` still returns the small, copyable `Size` of two `LayoutLen`s. diff --git a/docs/PLAN.md b/docs/PLAN.md index 66c1ef5..ec748af 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -872,7 +872,12 @@ is its *placement*, and the length a fraction resolves against is its *rel base*. A container says all three in one `PlaceDesc` argument. `Widget::draw` remains the only layout body. PR #19 also has optional size-request discovery for deferred min/max/clamp expressions (2026-09-20); ordinary widgets still -return `Size`, and plain requests retain symbolic allocation. The invariants, +return `Size`, and plain requests retain symbolic allocation. Built-in +min/max/clamp rules also constrain the offered box; the dimension helpers +edit the widget's own attributes, replacing the `MaxSize` wrapper. Preferred +lengths and bounds are independent, so setting `.width` preserves `.max_width` +and vice versa. Bound choices are retained as resolved lengths so moving a +drawing cannot resolve them against a different box. The invariants, the measured costs, the fuzzing method and the failed hypotheses are all in `docs/LAYOUT.md`; the app's pin at `32f6ad8` predates every part of it, so nothing here depends on it until the