Replace the step 3 plan with the one-ask protocol and record why it failed
The handoff now describes wip/one-ask at 3091fb8: a widget draws once in the box it is asked in, its answer is placed by re-expressing the drawing, and nothing is drawn again in a box an answer chose. The log records what the step 3 plan got wrong -- it kept the second draw whose measurement bit could not be defined, demanded a contract of the answer box, and narrowed frames by region -- and what fuzzing the new protocol found: the Part::Of composition dropping a pinned length, and a local redraw that must be put back in the parent's answer box. The Pad frame rule is left as a decision for Bryan with its cost stated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
39978ff8ea
commit
ef324aa88b
3 files changed
+370
-319
No files matched your search
+255
-298
@@ -4,32 +4,33 @@ Where the Iris retained-layout work stands for a worker picking it up cold.
|
|||||||
This file contains current decisions, the implementation plan and its checks.
|
This file contains current decisions, the implementation plan and its checks.
|
||||||
The durable layout design and measurement method are in `docs/LAYOUT.md`.
|
The durable layout design and measurement method are in `docs/LAYOUT.md`.
|
||||||
The temporary investigation record is in `docs/LAYOUT_LOG.md`; delete that
|
The temporary investigation record is in `docs/LAYOUT_LOG.md`; delete that
|
||||||
log when transparent frames lands, after moving any fact that must survive.
|
log when the one-ask protocol lands, after moving any fact that must survive.
|
||||||
|
|
||||||
## Where things stand
|
## Where things stand
|
||||||
|
|
||||||
Canonical upstream Iris `main` is **`ca2b4b2`** (#17, the headless rig). PR
|
Canonical upstream Iris `main` is **`ca2b4b2`** (#17, the headless rig). PR
|
||||||
#18's pushed branch is `split/18-position-chain` at **`e44dea3`**. Its
|
#18's pushed branch is `split/18-position-chain` at **`e44dea3`**. Its
|
||||||
detached comparison checkout is `/home/bob/repos/iris-layout-baseline` (the
|
detached comparison checkout is `/home/bob/repos/iris-layout-baseline`. It is
|
||||||
untracked `target-own/` there is its build output). It is the reviewed
|
the reviewed baseline this work must preserve or improve.
|
||||||
baseline this work must preserve or improve. `/home/bob/repos/iris-pr18` has
|
|
||||||
since moved to a different WIP branch; do not use that checkout as #18.
|
|
||||||
|
|
||||||
The continuation is `/home/bob/repos/iris-layout-experiment`, branch
|
The continuation is `/home/bob/repos/iris-layout-experiment`, now on branch
|
||||||
`wip/transparent-frames`, committed head **`4328eac`**, five commits over
|
**`wip/one-ask`** at **`3091fb8`**, one commit over `4328eac` (the head of
|
||||||
**`34cafb6`**, plus an uncommitted step 3/4 experiment. It implements
|
`wip/transparent-frames`, which is unchanged). It replaces the step 3 plan
|
||||||
transparent frames, pins the two open failures, and corrects `Scroll`'s
|
below with a smaller protocol change, and it passes every fast check:
|
||||||
content length, but it is not ready to replace #18:
|
|
||||||
|
|
||||||
- the uncommitted experiment passes both focused regressions and the 114-test
|
| check at `3091fb8` | result |
|
||||||
suite, but every non-ignored fast-oracle case stops on a cold-layout
|
| --- | --- |
|
||||||
placement assertion, usually at `Text`;
|
| `cargo fmt --all --check`, clippy `-D warnings`, with and without `layout-diagnostics` | clean |
|
||||||
- `many` and `resize` still do more work than #18 on a deep tree;
|
| `cargo test --workspace` (debug) | 114 suite, 20 core, 11 generated, all green |
|
||||||
- step 3 exposed a widget-contract question that must be planned before the
|
| the two decided-box pins from step 1 | **pass** (red at `4328eac`) |
|
||||||
retained protocol can change; see **Step 3 stop** below.
|
| `cargo test --release --test generated` | 11/11 |
|
||||||
|
| shrinker, 400 seeds, depth 5, all fifteen cases | agree, 56 s |
|
||||||
|
| 1000 seeds at depth 6, and the 2000-seed depth-4 scan | see `docs/LAYOUT_LOG.md`; they were running when this was written |
|
||||||
|
|
||||||
The app's Iris pin is unchanged. Keep the experiment's uncommitted changes as
|
The worker's uncommitted step 3/4 experiment is preserved as branch
|
||||||
evidence; do not commit them as the protocol until the stop below is resolved.
|
`wip/step3-experiment` (one commit over `4328eac`) and as
|
||||||
|
`~/repos/iris-step3-experiment.patch`. It is evidence, not the protocol.
|
||||||
|
The app's Iris pin is unchanged.
|
||||||
|
|
||||||
## The two rules to protect
|
## The two rules to protect
|
||||||
|
|
||||||
@@ -42,314 +43,287 @@ These outrank the accumulated machinery:
|
|||||||
of the room left after every sibling's `px` and `rel` lengths are resolved.
|
of the room left after every sibling's `px` and `rel` lengths are resolved.
|
||||||
|
|
||||||
Do not fix a failure with a tolerance, another measurement flag, a special
|
Do not fix a failure with a tolerance, another measurement flag, a special
|
||||||
case in `Span`, or another layout method. The investigation tried those
|
case in `Span`, or another layout method.
|
||||||
shapes and found that the protocol was asking an unanswerable question.
|
|
||||||
|
## What the previous plan got wrong
|
||||||
|
|
||||||
|
The full account is in `docs/LAYOUT_LOG.md`. The short version, because it
|
||||||
|
is the third plan for this repair and the next one should not repeat it:
|
||||||
|
|
||||||
|
- **Every plan kept the second draw.** The old protocol drew a widget in the
|
||||||
|
box it was asked in, then drew it *again* in the box its own answer placed
|
||||||
|
it in whenever the first drawing's `Holds` did not cover that box. All the
|
||||||
|
offer machinery -- `offer_place`, `offer_part`, `at_offer`, `measured()`,
|
||||||
|
the local-redraw deferral -- existed to remember which of the two draws was
|
||||||
|
the question. The plans tried to define that bit better; the defect was
|
||||||
|
that there were two draws at all.
|
||||||
|
- **The step 3 plan then over-corrected.** It said "every drawing must hold
|
||||||
|
for the answer box it supplies", and the worker implemented exactly that as
|
||||||
|
an assertion in `place`. A wrapped `Text` asked at 45 px whose longest word
|
||||||
|
is 89.5 px cannot satisfy it, and neither can any widget that reads its box
|
||||||
|
and reports something other than it. The answer box is not a question, so
|
||||||
|
no contract about it can be demanded of the widget.
|
||||||
|
- **It also let a caller narrow a frame by position.** A frame narrowed to a
|
||||||
|
region (the worker's share frames) does not move when the part it sits in
|
||||||
|
moves; only a frame narrowed to a *length*, put back into the part on
|
||||||
|
every placement, does.
|
||||||
|
|
||||||
|
## The protocol now in the experiment
|
||||||
|
|
||||||
|
**A widget draws once, in the box it is asked in. Its answer is placed inside
|
||||||
|
that box by re-expressing the drawing. Nothing is drawn again in a box an
|
||||||
|
answer chose.** `Holds` is a contract about the ask box alone, consulted only
|
||||||
|
to decide whether a re-ask can be skipped. This is `draw_inner` at `3091fb8`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let reused = (!stale)
|
||||||
|
.then(|| self.retained_answer(id, part, info))
|
||||||
|
.flatten()
|
||||||
|
.and_then(|answer| {
|
||||||
|
let extent = placed_extent(part, answer.0, declared, info.fill(), align);
|
||||||
|
self.try_reuse(id, frame, part, extent, info, rsc)
|
||||||
|
.map(|()| answer)
|
||||||
|
});
|
||||||
|
let answer = reused.unwrap_or_else(|| {
|
||||||
|
if old.is_none() {
|
||||||
|
old = self.remove(id, false, rsc);
|
||||||
|
}
|
||||||
|
let answer = self.draw_at(id, part, info, old.take(), rsc);
|
||||||
|
let extent = placed_extent(part, answer.0, declared, info.fill(), align);
|
||||||
|
if extent != part {
|
||||||
|
self.reposition(id, frame, extent, info, rsc);
|
||||||
|
}
|
||||||
|
answer
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`try_reuse` checks the drawing against `part` and relocates it to `extent`;
|
||||||
|
the old `place` (redraw in the answer box) is gone, and with it every offer
|
||||||
|
field's purpose. `ActiveData` keeps `offer_part` as the ask box, `offer_place`
|
||||||
|
as where it was asked and `place` as where it was put; the names are the old
|
||||||
|
ones and should be renamed (`part`, `asked`, `placed`) when this lands.
|
||||||
|
|
||||||
|
A container that puts an answer somewhere other than where it asked says so
|
||||||
|
with a new call that never runs the body:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Puts a child asked about in this draw somewhere else in this
|
||||||
|
/// widget's box: its answer, placed in this part instead. The drawing
|
||||||
|
/// is re-expressed there rather than made again -- what a row does once
|
||||||
|
/// it knows every slot, having measured each child from its cursor.
|
||||||
|
pub fn place_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, place: [Place; 2])
|
||||||
|
```
|
||||||
|
|
||||||
|
A frame is narrowed by a *length* of the parent's frame, never a region, and
|
||||||
|
is put back into the part by the child's alignment on every placement:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn widget_at<'s, W: ?Sized>(
|
||||||
|
&'s mut self,
|
||||||
|
id: &'s StrongWidget<W>,
|
||||||
|
narrow: [Option<Len>; 2],
|
||||||
|
place: [Place; 2],
|
||||||
|
) -> DrawResult<'s, 'a, W>
|
||||||
|
```
|
||||||
|
|
||||||
|
`Span` asks every child once from its cursor (`Within(From(cursor..far))`),
|
||||||
|
then moves fixed children to their slots and asks share children once more
|
||||||
|
in their decided slot with the frame narrowed to it:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let slot = along(from, start);
|
||||||
|
let place = axis.pair(Place::Fill(Part::From(slot)), across);
|
||||||
|
let used = match len.leftover > Weight::ZERO && shares {
|
||||||
|
true => {
|
||||||
|
let mut narrow = [None; 2];
|
||||||
|
narrow[axis as usize] = Some(slot.len());
|
||||||
|
painter.widget_at(child, narrow, place).len(!axis)
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
painter.place_at(child, place);
|
||||||
|
size.axis(!axis)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`Stack` asks non-sizing children in the box the sizing child decided, with
|
||||||
|
the frame narrowed to it on every axis that is not a share, and `Scroll` asks
|
||||||
|
its content once in the viewport and `place_at`s it to the scrolled offset.
|
||||||
|
|
||||||
|
A local redraw asks the retained question again -- the same place of the box
|
||||||
|
the parent was *asked* in -- and, if the answer stands, puts the fresh drawing
|
||||||
|
back at the retained place of the box the parent's answer *chose*. Both halves
|
||||||
|
are needed: seed 2 at depth 4 (a stack sized by its text) fails without the
|
||||||
|
second.
|
||||||
|
|
||||||
|
A symbolic length a child pinned now composes through `Part::Of` where the
|
||||||
|
part is the whole box less pixels, and pins the parent's own length otherwise
|
||||||
|
(`in_parent`). Dropping it let a zero `Pad` reuse a drawing across a narrowed
|
||||||
|
frame of the same pixel length; the shrinker found six such seeds at depth 5.
|
||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
Decided with Bryan on 2026-09-17 and 2026-09-18.
|
Decided with Bryan on 2026-09-17 and 2026-09-18, kept where still true.
|
||||||
|
|
||||||
### One draw method, in a box decided from above
|
### One draw method, in a box decided from above
|
||||||
|
|
||||||
`Widget::draw` remains the only layout method. A second measure method would
|
`Widget::draw` remains the only layout method. A container's body runs only
|
||||||
duplicate layout and drift from drawing; a shared helper would merely move
|
in a box its parent offered or decided, never in a box derived from the
|
||||||
that obligation without removing it.
|
container's own answer. **The experiment extends this to every widget:** a
|
||||||
|
leaf is not drawn in its answer box either. Its drawing is re-expressed
|
||||||
A container's body runs only in a box its parent offered or decided, never in
|
there, which for a text means the block it shaped at the asked width is
|
||||||
a box derived from the container's own answer. Measuring asks may be
|
positioned inside the box its reported size chose, and its lines do not
|
||||||
provisional while a parent is dividing room. Once the parent decides a slot,
|
change. `examples/text.rs` and `random` have not been rendered since; do that
|
||||||
the child is evaluated in that slot; placing an answer is reuse or translation,
|
before landing and inspect any change.
|
||||||
not another `draw_at` in an answer-derived box.
|
|
||||||
|
|
||||||
The current `offer` bit cannot express this. It is derived from
|
|
||||||
`place == offer_place`, but one parent draw can evaluate a child in the
|
|
||||||
parent's room, in a slot the parent decided, and in a box derived from the
|
|
||||||
child's own answer. The fresh answer is right in some of those boxes and a
|
|
||||||
retained answer in others. Remove the question rather than adding state that
|
|
||||||
tries to answer it.
|
|
||||||
|
|
||||||
The target retained model has one answer per widget. Its `Holds` contract says
|
|
||||||
which parts it remains valid for. `redraw` re-asks it in the part of its last
|
|
||||||
parent ask. The offer machinery can then go: answer gating, `offer_place`,
|
|
||||||
`offer_part`, `at_offer`, and `measured()`.
|
|
||||||
|
|
||||||
Keep `Part::Of`. It expresses a part of a widget's own box without making the
|
|
||||||
container read that box's length, and was a sound addition to the experiment.
|
|
||||||
Its dropped `extent_len` pin exposed seed 2, but composing every such pin
|
|
||||||
through `Of` is not the repair: that experiment broke seed 220 and the
|
|
||||||
region-node regression.
|
|
||||||
|
|
||||||
### Frames are narrowed by every length decided from above
|
### Frames are narrowed by every length decided from above
|
||||||
|
|
||||||
Containers that only divide room are transparent: absent a length decision,
|
A declared `px` or `rel`, a resolved share, and (new, planner's choice, not
|
||||||
they forward the parent's frame. A declared `px` or `rel` length narrows the
|
yet Bryan's) the box a stack's sizing child decided all narrow the frame.
|
||||||
child's frame. **A resolved `leftover` share narrows it in exactly the same
|
`declared_lens` still excludes `leftover`, which is right: a share has no
|
||||||
way.** The code's exclusion of `leftover` in `declared_lens` is a bug.
|
length until the span divides its room, and it narrows the frame at the
|
||||||
|
placing ask instead.
|
||||||
|
|
||||||
A share is known only after the deciding span has measured fixed children and
|
### `Pad` remains an outset -- and is not yet what was decided
|
||||||
divided its room. The measuring ask therefore cannot settle a `leftover`
|
|
||||||
child's frame. The placing ask supplies the resolved share as both its slot
|
|
||||||
and narrowed frame, and fully evaluates the child there. This matters across
|
|
||||||
the span too: a wrapping child's height may change once its width share is
|
|
||||||
known, so the span reads that child's cross-axis answer from the decided-box
|
|
||||||
evaluation, not from the provisional one.
|
|
||||||
|
|
||||||
A box a widget merely reports does not narrow its descendants' frames. The
|
Bryan: padding goes outside what it pads; the pad forwards its frame less the
|
||||||
frame changes because a parent decided a declared length or a share, not
|
padding and draws the child inside that area, so `rel(1.0)` inside `.pad(16)`
|
||||||
because the child's own answer happened to have that length.
|
inside a 450 px share is 418 px. **The experiment does not implement this.**
|
||||||
|
`Pad` still forwards the frame whole and insets only the box:
|
||||||
|
|
||||||
### `Pad` remains an outset
|
```rust
|
||||||
|
let inset = |lead: Px, trail: Px| {
|
||||||
|
Place::Within(Part::Of(UiSpan::new(
|
||||||
|
Len::from_parts(Rel::ZERO, lead),
|
||||||
|
Len::from_parts(Rel::ONE, -trail),
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
Padding goes outside what it pads. There is no mixed "outset pixels, inset
|
so `rel(1.0)` there is 450 and overflows by the padding -- the clipped
|
||||||
fractions and shares" `Pad`.
|
`text.rs` render. The decided rule cannot be written in the current
|
||||||
|
representation without a cost Bryan has not seen:
|
||||||
|
|
||||||
The clipped `examples/text.rs` render is evidence of the unresolved-share
|
- Narrowing the child's frame by the padding (`narrow = FULL - 32px`) makes
|
||||||
bug, not intended behavior and not a reason to replace `rel(1.0)` with
|
that length the child's *box* too, since a narrowed frame is its own box.
|
||||||
`leftover`. If a 900 px row gives a padded child a 450 px share, that share
|
In a share or a declared box, frame and box coincide and the rule holds.
|
||||||
narrows the pad's frame; after 16 px padding on each side, `rel(1.0)` inside
|
As a fixed child of a span measured from its cursor, the padded child is
|
||||||
the pad is 418 px and fits. Keep the example so it verifies that rule.
|
then asked in `row - 32` rather than `room - 32`: a padded wrapped text
|
||||||
|
after a 24 px icon wraps as if the icon were not there and overflows the
|
||||||
|
row by 24 px. That row is the commonest thing in the app.
|
||||||
|
- Keeping `Part::Of` (status quo) keeps the text wrapping in the room and
|
||||||
|
makes `rel(1.0)` under a pad mean the whole frame.
|
||||||
|
- Having both -- `rel` of the frame less padding *and* a box that is the
|
||||||
|
room less padding -- needs the frame to be a length and the box a region in
|
||||||
|
the parent's coordinates, with `Part::From` scaling its span by the frame
|
||||||
|
length. That is a coordinate rewrite of `painter.rs`/`render_state.rs`, not
|
||||||
|
a widget change.
|
||||||
|
|
||||||
|
Ask Bryan which. Do not decide it in a worker session.
|
||||||
|
|
||||||
### A share never adds room beyond the deciding box
|
### A share never adds room beyond the deciding box
|
||||||
|
|
||||||
`Scroll` currently calls `apply_leftover`, which turns any `leftover` in the
|
`Scroll` resolves its content length from the fixed part of the answer and
|
||||||
content answer into a whole additional viewport. A row reporting `600 px +
|
makes it at least the viewport (`4328eac`). Unchanged.
|
||||||
leftover` in a 900 px viewport therefore gets a 1500 px content box, its text
|
|
||||||
re-wraps in room it was not measured in, and layout advances one fixed-point
|
|
||||||
iteration each time it is evaluated.
|
|
||||||
|
|
||||||
That behavior is wrong. A share takes the room left in the viewport. Resolve
|
|
||||||
scroll content from only the fixed part of the answer and make it at least the
|
|
||||||
viewport:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let fixed = Len::from_parts(answer_len.rel, answer_len.px).to_px(container_len);
|
|
||||||
self.content_len = fixed.max(container_len);
|
|
||||||
```
|
|
||||||
|
|
||||||
For `600 px + leftover` this is 900 px: the share receives 300 px and nothing
|
|
||||||
scrolls. Pixel content still scrolls when its fixed length exceeds the
|
|
||||||
viewport. A lone share fills the viewport. The content box no longer invents
|
|
||||||
new room, so `Scroll` is not an own-answer-box exception to the decided-box
|
|
||||||
rule.
|
|
||||||
|
|
||||||
### Existing fixed-point and box-chain design stays
|
### Existing fixed-point and box-chain design stays
|
||||||
|
|
||||||
The fixed-point grid, `Holds::through`, the pixel box threaded down the draw,
|
Unchanged; see `docs/LAYOUT.md`.
|
||||||
region nodes, and the warm/cold equality rule are sound. Their durable
|
|
||||||
invariants are in `docs/LAYOUT.md`. Transparent frames fixed fraction
|
|
||||||
resolution; the remaining defect is evaluating container bodies in multiple
|
|
||||||
boxes and trying to infer which evaluation counted as measurement.
|
|
||||||
|
|
||||||
## Why the open seeds fail
|
|
||||||
|
|
||||||
The complete traces, shrunk trees and counter-experiments are in
|
|
||||||
`docs/LAYOUT_LOG.md`. The short version a worker needs is:
|
|
||||||
|
|
||||||
- **Seed 2 (`repaint`)**: a stack measures a non-sizing subtree in room the
|
|
||||||
stack will never have, then reuses that answer in the stack's one-line box.
|
|
||||||
A dropped `Part::Of` pin makes the reuse look valid. Composing that pin fixes
|
|
||||||
this seed but breaks seed 220 and an existing region-node test, so it is not
|
|
||||||
the fix.
|
|
||||||
- **Seed 108 (`reorder`)**: a nested span correctly evaluates a branch in its
|
|
||||||
final 300 px box, but `draw_inner` discards the fresh 286 px answer for a
|
|
||||||
retained 438.9 px answer from an earlier 450 px evaluation because the
|
|
||||||
place expression changed. Always keeping the fresh answer fixes this seed
|
|
||||||
but breaks four seeds and two tests under `Scroll`.
|
|
||||||
- Those `Scroll` failures are the `apply_leftover` feedback loop above. Once
|
|
||||||
that loop is removed, no legitimate container needs to lay children out in
|
|
||||||
a box derived from its own answer.
|
|
||||||
|
|
||||||
The worker faithfully implemented the earlier plan, tried four definitions
|
|
||||||
of "measurement", restored the safe deferral when each failed elsewhere, and
|
|
||||||
stopped. Do not resume that search.
|
|
||||||
|
|
||||||
## Implementation plan
|
## Implementation plan
|
||||||
|
|
||||||
Work in `/home/bob/repos/iris-layout-experiment` from `49cec82`. Make each
|
Work in `/home/bob/repos/iris-layout-experiment` on `wip/one-ask` from
|
||||||
step a warning-clean commit and run its named checks before the next. If a
|
`3091fb8`. Make each step a warning-clean commit and run its named checks
|
||||||
step exposes a different mechanism, stop and update this handoff rather than
|
before the next. If a step exposes a different mechanism, stop and update
|
||||||
papering over it.
|
this handoff rather than papering over it.
|
||||||
|
|
||||||
### 1. Pin the two failures as focused tests
|
### 1. Read the long fuzzer results
|
||||||
|
|
||||||
**Done in `b842e4f`.** The named tests reproduce the mismatch at `49cec82` and
|
`docs/LAYOUT_LOG.md` records the 1000/6 oracle and the 2000/4 scan at
|
||||||
remain intentionally red until the protocol repair:
|
`3091fb8` if they finished. If either found a seed, shrink it first
|
||||||
|
(`SHRINK_SEED=<seed> SHRINK_DEPTH=<depth> SHRINK_CASE=<case>`), pin it as a
|
||||||
|
named test in `tests/cases/unsettled.rs`, then fix it under the rule above.
|
||||||
|
Do not add a second draw back.
|
||||||
|
|
||||||
- `unsettled::repainting_a_stack_uses_the_box_its_sizing_child_decided`;
|
### 2. Render and replay
|
||||||
- `unsettled::reordering_nested_spans_keeps_the_answer_from_the_decided_box`.
|
|
||||||
|
|
||||||
Turn the shrunk seed 2 and seed 108 trees from `docs/LAYOUT_LOG.md` into fast,
|
Read the installed graphics skill and confirm the renderer. Render `view`,
|
||||||
named regression tests. Each must demonstrate the present warm/cold mismatch
|
`minimal`, `random`, `tabs` and `text` at 1920x1200 against `34cafb6` and
|
||||||
at `49cec82`, then pass because both paths select the same tree and boxes—not
|
`e44dea3`, replay `tabs`, and compare a live resize of `random` with a cold
|
||||||
because the assertion was weakened.
|
render at the same size (commands under **Full verification** below). A
|
||||||
|
text placed by re-expression rather than a second draw is the change most
|
||||||
|
likely to show here; inspect every intentional difference and record it.
|
||||||
|
|
||||||
Also retain these nearby regression tests while changing the protocol:
|
### 3. Rename and delete
|
||||||
|
|
||||||
- `unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`
|
Rename `ActiveData::offer_part` to `part`, `offer_place` to `asked`, `place`
|
||||||
- `unsettled::a_span_given_the_box_its_answer_decided_matches_a_cold_layout`
|
to `placed`, and `DrawInfo` likewise; delete `ActiveData::measured` in favour
|
||||||
- the seed 86 `Scroll` fixed-point case
|
of reading `answer`; delete `answers_at` if `resize` is its only caller and
|
||||||
- the tests for a length in pixels staying that many pixels and for an exact
|
inline it. Every use was written against the old names on purpose to keep
|
||||||
leftover split
|
the probe's diff readable; do this as one mechanical commit. Suite, oracle.
|
||||||
|
|
||||||
Check the ordinary suite and each new test individually.
|
### 4. Restore the expected retained cost
|
||||||
|
|
||||||
### 2. Correct `Scroll`'s content length
|
Work counters at `3091fb8`, seed 1 and 13, depth 8, widget draws / distinct
|
||||||
|
widgets, beside `e44dea3` (#18) and `49cec82` (the branch head before this):
|
||||||
|
|
||||||
**Done in `4328eac`.** The four focused cases pass, as do the release fast
|
| seed 1 | e44dea3 | 49cec82 | 3091fb8 |
|
||||||
oracle and the depth-5 counterexample seeds 184, 246, 292 and 372. The debug
|
| --- | --- | --- | --- |
|
||||||
suite has 112 passing tests and only the two intentionally red tests above.
|
| cold | 369/261 | 516/288 | 331/288 |
|
||||||
|
| many | 157/95 | 187/119 | 78/78 |
|
||||||
|
| size | 16/12 | 3/3 | 3/3 |
|
||||||
|
| scroll | 2 | 1 | 1 |
|
||||||
|
| resize | 13/13 | 24/76 | 40/15 |
|
||||||
|
|
||||||
Replace its `apply_leftover` content sizing with the fixed-part calculation
|
| seed 13 | e44dea3 | 49cec82 | 3091fb8 |
|
||||||
above. Add focused cases for:
|
| --- | --- | --- | --- |
|
||||||
|
| cold | 1330/707 | 2940/982 | 1179/982 |
|
||||||
|
| many | 524/159 | 1091/423 | 335/333 |
|
||||||
|
| resize | nothing | 2215/510 | nothing |
|
||||||
|
|
||||||
- `600 px + leftover` in 900 px resolves to 900 px;
|
`many` and `size` are better than #18 at seed 1 and `many` draws fewer times
|
||||||
- fixed content wider than the viewport still scrolls;
|
at seed 13, but it touches twice as many distinct widgets there, and `resize`
|
||||||
- a lone `leftover` child fills without scrolling;
|
at seed 1 draws 40 times where #18 drew 13. Two mechanisms, both understood:
|
||||||
- the wrapping-text-plus-share case is stable warm and cold.
|
|
||||||
|
|
||||||
The seed 86 test stays until the full protocol has landed, even if its old
|
- **A share child is asked twice per span draw** -- in the measuring room
|
||||||
special rule becomes moot.
|
with the frame forwarded, then in its slot with the frame narrowed. Each
|
||||||
|
ask that reads pixels or pins a length draws. Give `Span` a measure-only
|
||||||
|
ask for the first pass: reuse the retained *answer* when its holds contain
|
||||||
|
the room, without validating or relocating the drawing, and let the
|
||||||
|
placing ask settle the drawing. The answer contract must then carry no
|
||||||
|
symbolic pin (a span's total does not depend on `far`; only its slots do),
|
||||||
|
which is the separation the worker's experiment made with
|
||||||
|
`answer_extent_len`. Measure `many` at seed 13 before and after.
|
||||||
|
- **A positive-direction span with no shares pins `far`** it does not need,
|
||||||
|
so a resize redraws it. Read `extent_len` only where a slot depends on it
|
||||||
|
(shares, or `Sign::Neg`), and express the measuring room's far end without
|
||||||
|
the length. Measure `resize` at seed 1 before and after.
|
||||||
|
|
||||||
Check the suite, fast oracle, and the known Scroll counterexamples from the
|
Report every phase at both seeds, work counters first, medians only when the
|
||||||
fresh-answer experiment (seeds 184, 246, 292 and 372).
|
work agrees.
|
||||||
|
|
||||||
### 3. Evaluate children in parent-decided boxes
|
### 5. Full verification and landing
|
||||||
|
|
||||||
**STOP: this step needs planning before implementation continues.** The first
|
|
||||||
17 suite failures were not sufficient evidence for stopping: most were test
|
|
||||||
fixtures that encoded the old paired-draw cost while their empty drawing used
|
|
||||||
only the answer. The experiment now separates answer-only pixel reads from
|
|
||||||
drawing reads, keeps answer and final-drawing contracts independent, asks
|
|
||||||
`Span` children provisionally with `Fill`, re-asks them in their decided
|
|
||||||
slots, narrows resolved-share frames, and re-asks local redraws at the
|
|
||||||
question their answer came from. It also fixes two real defects found along
|
|
||||||
the way: an overfull span falsely pinned itself to `far`, and a narrowed share
|
|
||||||
frame omitted a nonzero parent-extent start.
|
|
||||||
|
|
||||||
That version passes the full 114-test suite, both focused regressions, the
|
|
||||||
region-node test, the one-pixel chain, and the wrap-at-remaining-room test.
|
|
||||||
The tests were checked individually before migration; geometry assertions
|
|
||||||
were retained, and drawing-dependency fixtures still use ordinary `px_len`.
|
|
||||||
|
|
||||||
The release fast oracle then stopped in all eleven non-ignored cases during
|
|
||||||
*cold layout*, before any warm/cold comparison. The repeated shape is a leaf,
|
|
||||||
usually `Text`, asked provisionally in a `Within` box, reporting a smaller
|
|
||||||
answer, whose retained drawing contract covers only the provisional pixel
|
|
||||||
width. Placement into the reported box therefore hits the new
|
|
||||||
reuse-or-translate assertion. One representative assertion shows `Text` with
|
|
||||||
an X part of `FULL`, a centered answer span about 89.5 px wide, and a drawing
|
|
||||||
extent contract pinned at 45 px. A generated `Stack` also fails after
|
|
||||||
reporting a 178x21 px answer from `FULL` while retaining an `extent_len` pin
|
|
||||||
to `FULL`.
|
|
||||||
|
|
||||||
This is a real public-contract boundary, not a stale oracle: decide whether
|
|
||||||
every leaf as well as every container must guarantee that its provisional
|
|
||||||
drawing holds in the answer box it reports, and specify how `Text` establishes
|
|
||||||
that guarantee, or allow some answer-derived leaf evaluation while keeping
|
|
||||||
containers out of their own answers. The latter is a semantic distinction the
|
|
||||||
current `Widget` API does not express. Do not weaken the assertion, special
|
|
||||||
case `Text`, or resume the oracle until this choice is written into the plan.
|
|
||||||
|
|
||||||
Change placement so an answer-derived box never runs a container body.
|
|
||||||
Placing becomes reuse-or-translate. Remove the offer/measurement gate and
|
|
||||||
its retained bookkeeping only as each caller stops needing it; do not leave
|
|
||||||
a parallel old path.
|
|
||||||
|
|
||||||
Three current widgets must stop depending on measuring boxes they will never
|
|
||||||
own:
|
|
||||||
|
|
||||||
- `Span`: do not read `extent_len` unconditionally. Slots depend on `far`
|
|
||||||
only when shares exist (the decided slot fills its part) or for negative
|
|
||||||
direction; compute negative-direction slots from `total`. Pin the extent
|
|
||||||
length only in those cases.
|
|
||||||
- `Stack`: draw non-sizing children in `From(0..size)` on an axis where the
|
|
||||||
sizing child's answer is `px`/`rel`, and `All` where it is `leftover`,
|
|
||||||
instead of drawing them in `All` of the measuring room.
|
|
||||||
- `Branch` in the random rig: express "the rest of my box" as
|
|
||||||
`Of(40px..FULL)` rather than reading `extent_len(Y)`.
|
|
||||||
|
|
||||||
Every drawing must hold for the answer box it supplies. The two focused tests
|
|
||||||
from step 1 and the existing region-node and decided-box tests must pass here.
|
|
||||||
|
|
||||||
### 4. Make resolved shares narrow frames
|
|
||||||
|
|
||||||
Give a `leftover` child its resolved slot as its narrowed frame at the placing
|
|
||||||
ask. A span becomes a decided two-pass layout:
|
|
||||||
|
|
||||||
1. measure fixed children and collect share weights;
|
|
||||||
2. divide the deciding box's remaining room;
|
|
||||||
3. place/evaluate each child in its decided box, with a share child's frame
|
|
||||||
narrowed to that share;
|
|
||||||
4. derive the span's cross-axis answer from those decided evaluations where a
|
|
||||||
child's answer can depend on its share.
|
|
||||||
|
|
||||||
Do not put `leftover` back into a declaration helper before it has a resolved
|
|
||||||
length; unlike `px` and `rel`, its frame cannot be known during the first pass.
|
|
||||||
|
|
||||||
Add tests that a `rel(1.0)` child directly inside a half share is half the row,
|
|
||||||
and that the same child inside `.pad(16)` is the share less 32 px. The existing
|
|
||||||
`examples/text.rs` case should render inside its padding without changing its
|
|
||||||
width rule.
|
|
||||||
|
|
||||||
### 5. Remove obsolete machinery and settle the retained path
|
|
||||||
|
|
||||||
Once all callers use the decided-box path, delete answer gating,
|
|
||||||
`offer_place`, `offer_part`, `at_offer`, `measured()`, and the local-redraw
|
|
||||||
deferral whose only purpose was distinguishing measurement from placement.
|
|
||||||
Write `ActiveData::answer` in one place, and keep `DrawInfo` on `ActiveData`
|
|
||||||
rather than copying fields and reconstructing it in `redraw`.
|
|
||||||
|
|
||||||
Run the ordinary suite, fast oracle and shrinker before doing performance
|
|
||||||
work. Both new focused tests must pass on cold, repaint and reorder paths.
|
|
||||||
|
|
||||||
### 6. Restore the expected retained cost
|
|
||||||
|
|
||||||
Implement the known-length `Span` shortcut only after correctness is stable:
|
|
||||||
a `px` child after a `px` child should draw once cold and never on repaint.
|
|
||||||
Report cross-axis sizes from the decided evaluation, including wrapping
|
|
||||||
share children. Compare all six diagnostic phases with `e44dea3`; investigate
|
|
||||||
work-counter differences before interpreting time.
|
|
||||||
|
|
||||||
Expected direction, not a license to weaken correctness: `size` and `scroll`
|
|
||||||
keep their wins, `many` approaches #18's distinct-widget counts, and `resize`
|
|
||||||
returns to about #18's 13 draws at seed 1. Record final counters in the
|
|
||||||
temporary log and durable conclusions in `docs/LAYOUT.md`.
|
|
||||||
|
|
||||||
### 7. Full verification and landing
|
|
||||||
|
|
||||||
Run, in the experiment checkout:
|
Run, in the experiment checkout:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo fmt --all --check
|
cargo fmt --all --check
|
||||||
cargo clippy --workspace --all-targets -- -D warnings
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
cargo clippy --workspace --all-targets --features layout-diagnostics -- -D warnings
|
||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
cargo test --release --test generated
|
cargo test --release --test generated
|
||||||
SHRINK_CASE=all SHRINK_SEEDS=400 SHRINK_DEPTH=5 \
|
SHRINK_CASE=all SHRINK_SEEDS=400 SHRINK_DEPTH=5 \
|
||||||
cargo test --release --test shrink -- --ignored --nocapture
|
cargo test --release --test shrink -- --ignored --nocapture
|
||||||
IRIS_GENERATED_SEEDS=1000 IRIS_GENERATED_DEPTH=6 \
|
IRIS_GENERATED_SEEDS=1000 IRIS_GENERATED_DEPTH=6 \
|
||||||
cargo test --release --test generated -- --ignored a_long_run_of_seeds_agrees
|
cargo test --release --test generated -- --ignored a_long_run_of_seeds_agrees
|
||||||
|
SHRINK_CASE=all SHRINK_SEEDS=2000 SHRINK_DEPTH=4 \
|
||||||
|
cargo test --release --test shrink -- --ignored --nocapture
|
||||||
```
|
```
|
||||||
|
|
||||||
Then repeat the 2000-seed depth-4 scan over all fifteen cases. It is the only
|
The last line is the 2000-seed depth-4 scan over all fifteen cases; the
|
||||||
run that found seeds 1121 and 1839 before their fix; depth and breadth find
|
shrinker runs the same cases as the scan and reduces anything it finds, so
|
||||||
different defects. `Rng::new` uses `seed | 1`, so adjacent even/odd seed
|
no temporary test body is needed any more. `Rng::new` uses `seed | 1`, so
|
||||||
pairs describe the same tree. The temporary scan target used this body and
|
adjacent even/odd seed pairs describe the same tree.
|
||||||
was deleted after the run:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
over_seeds((1..=2000).collect(), |seed| {
|
|
||||||
let grown = plan(seed, 4, &Edits::default());
|
|
||||||
for &case in ALL.iter() {
|
|
||||||
if let Some(how) = diverges(&grown, case, seed) {
|
|
||||||
println!("HIT seed {seed} case {} {how}", case.name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Run all six `layout_diagnostics` phases at seeds 1 and 13, depth 8, against
|
|
||||||
`e44dea3`. Compare work counters first; use medians only after the work is the
|
|
||||||
same. Check that `UiSpan::within` still inlines with `nm`.
|
|
||||||
|
|
||||||
Render `view`, `minimal`, `random`, `tabs` and `text` at 1920x1200 and inspect
|
Render `view`, `minimal`, `random`, `tabs` and `text` at 1920x1200 and inspect
|
||||||
every intentional change. Also replay `tabs` and compare a live resize of
|
every intentional change. Also replay `tabs` and compare a live resize of
|
||||||
@@ -377,35 +351,18 @@ The reference replay is:
|
|||||||
880 up 1836 1116
|
880 up 1836 1116
|
||||||
```
|
```
|
||||||
|
|
||||||
Before submitting, run the pre-submit review. Once transparent frames lands,
|
Before submitting, run the pre-submit review. Once the protocol lands, move
|
||||||
move any surviving fact from `docs/LAYOUT_LOG.md` into `docs/LAYOUT.md`, delete
|
any surviving fact from `docs/LAYOUT_LOG.md` into `docs/LAYOUT.md`, delete
|
||||||
the log, update this handoff to the next actual task, update the app's Iris pin
|
the log, update this handoff to the next actual task, update the app's Iris
|
||||||
only when the Iris change is ready, and push every coherent commit.
|
pin only when the Iris change is ready, and push every coherent commit.
|
||||||
|
|
||||||
## Verification already performed
|
|
||||||
|
|
||||||
At `49cec82`: formatting, warning-clean clippy, the debug suite (108 suite
|
|
||||||
tests and 20 core tests), the 11 generated cases, and all six diagnostic
|
|
||||||
phases ran. The 400-seed depth-5 shrinker fails at seed 2 (`repaint`) and 108
|
|
||||||
(`reorder`); the long 1000/6 oracle and 2000/4 scan were intentionally not
|
|
||||||
run after that. Reference renders against `34cafb6`: `view` and `minimal`
|
|
||||||
were byte-identical; `tabs` differed by 2,332 pixels; `text` exposed the
|
|
||||||
unresolved-share bug; `random` moved where nested spans do.
|
|
||||||
|
|
||||||
At `e44dea3`, the #18 baseline: format, clippy and workspace tests passed;
|
|
||||||
the release oracle at 100 seeds, debug oracle at 120 seeds, all fifteen
|
|
||||||
shrinker cases at 400/5, the oracle at 1000/6, and the 2000/4 scan passed.
|
|
||||||
The five reference renders and the `tabs` replay were byte-identical to their
|
|
||||||
baseline, and a live-resized `random` matched a cold render. These statements
|
|
||||||
describe those commits only; rerun them after changing layout.
|
|
||||||
|
|
||||||
## Follow-on work, not part of this repair
|
## Follow-on work, not part of this repair
|
||||||
|
|
||||||
|
- The `Pad` decision above, and the coordinate rewrite if Bryan wants both
|
||||||
|
halves of the rule.
|
||||||
- CPU round-to-nearest and shader nearest-pixel snapping are approved as one
|
- CPU round-to-nearest and shader nearest-pixel snapping are approved as one
|
||||||
separately verified change. Neither has landed. Re-derive `Holds::through`
|
separately verified change. Neither has landed. Re-derive `Holds::through`
|
||||||
for the new rounding and run both long fuzzers plus the render set.
|
for the new rounding and run both long fuzzers plus the render set.
|
||||||
- Test-only `Inset` and `Outset` can demonstrate their semantics after the
|
|
||||||
protocol is stable. `Pad` itself remains an outset.
|
|
||||||
- Smaller layout items remain in `docs/LAYOUT_LOG.md`: an undrawn share's
|
- Smaller layout items remain in `docs/LAYOUT_LOG.md`: an undrawn share's
|
||||||
gap, nested share weights, inconsistent zero-divisor fallbacks, and the
|
gap, nested share weights, inconsistent zero-divisor fallbacks, and the
|
||||||
stale `f32` identity comment.
|
stale `f32` identity comment.
|
||||||
|
|||||||
+9
-5
@@ -326,11 +326,15 @@ inside that area, and reports the child's used size plus padding. A
|
|||||||
share less that padding. The mixed "outset pixels, inset fractions and
|
share less that padding. The mixed "outset pixels, inset fractions and
|
||||||
shares" interpretation is rejected.
|
shares" interpretation is rejected.
|
||||||
|
|
||||||
The current experiment still redraws some widgets in boxes derived from their
|
A widget draws once, in the box it is asked in; its answer is placed inside
|
||||||
own answers. That is the open protocol defect, not a design invariant. The
|
that box by re-expressing the drawing, and nothing is drawn again in a box an
|
||||||
target in `docs/HANDOFF.md` evaluates container bodies only in boxes a parent
|
answer chose. `Holds` is a contract about the ask box alone, read only to
|
||||||
offered or decided; placing an answer reuses or translates its drawing rather
|
decide whether a re-ask can be skipped. A container that puts an answer
|
||||||
than running the body in an answer-derived box.
|
somewhere other than where it asked says so with `Painter::place_at`, which
|
||||||
|
never runs the body. A frame is narrowed by a length of the parent's frame,
|
||||||
|
never by a region, and is put back into the part by the child's alignment on
|
||||||
|
every placement. This is `wip/one-ask` in the experiment checkout; whether
|
||||||
|
`Pad` narrows the frame by its padding is still open in `docs/HANDOFF.md`.
|
||||||
|
|
||||||
## Layout decisions and invariants (2026-09-15 to 2026-09-17)
|
## Layout decisions and invariants (2026-09-15 to 2026-09-17)
|
||||||
|
|
||||||
|
|||||||
+106
-16
@@ -7,9 +7,91 @@ must outlive it (settled design, the measurement method) is already in
|
|||||||
`docs/LAYOUT.md`, and the current plan is in `docs/HANDOFF.md`. Commit ids
|
`docs/LAYOUT.md`, and the current plan is in `docs/HANDOFF.md`. Commit ids
|
||||||
are in `/home/bob/repos/iris-layout-experiment` unless said otherwise.
|
are in `/home/bob/repos/iris-layout-experiment` unless said otherwise.
|
||||||
|
|
||||||
## The two open shrinker seeds on `wip/transparent-frames` (planner, 2026-09-18)
|
## What the one-ask protocol found (planner, 2026-09-18, third session)
|
||||||
|
|
||||||
Both are the rule in `draw_inner` that decides which answer places a widget's
|
Branch `wip/one-ask` at `3091fb8` over `4328eac`. The change is described in
|
||||||
|
`docs/HANDOFF.md`; this is what building and fuzzing it turned up, in order.
|
||||||
|
|
||||||
|
- **The step 3 assertion was the wrong check.** At the worker's stopping
|
||||||
|
point every generated case stopped on `assert_eq!(extent, info.part)` in
|
||||||
|
`place`, on a `Text` asked at 45 px in `Within(All)` whose longest word is
|
||||||
|
89.5 px: its drawing holds for `[45, 45]` and its answer box is 89.5 px
|
||||||
|
wide. No leaf can promise its drawing holds for a box chosen from its
|
||||||
|
answer; the plan's sentence "every drawing must hold for the answer box it
|
||||||
|
supplies" was the defect, not `Text`. Removing the check *and the redraw
|
||||||
|
it guarded* -- placement is re-expression, unconditionally -- made the
|
||||||
|
suite green including both decided-box pins, with nothing else changed.
|
||||||
|
- **Six retained tests encoded the paired draw** (`settled + 2`, "drawn
|
||||||
|
once" expecting two): they now expect one draw. Two more failed because
|
||||||
|
`Stack` had been changed to ask non-sizing children with `Fill(All)`; they
|
||||||
|
need `Within(All)` so a smaller answer sits inside the stack, with the
|
||||||
|
frame narrowed to the sizing answer. That is the whole of the suite churn.
|
||||||
|
- **Seed 2 at depth 4 (`repaint-some`, `region-node`)** shrinks to a `Stack`
|
||||||
|
sized by one text. A local redraw of the text asked it again in the box
|
||||||
|
the stack was *asked* in and left it filling that; cold puts it in the box
|
||||||
|
the stack's *answer* chose. A local redraw must put the fresh drawing
|
||||||
|
back at the retained place of the parent's answer box, always, not only
|
||||||
|
when the asked and placed places differ.
|
||||||
|
- **Six seeds at depth 5** (60 `repaint`, 308 `every-size`, 20
|
||||||
|
`region-node`, 248, 384, 162 `reorder`) shrink to one shape: a zero `Pad`
|
||||||
|
round a widget reading `extent_len` (a span with a share rect, or a
|
||||||
|
`Branch`), as a share child of a span beside a fixed sibling. Seed 248
|
||||||
|
reproduced: **cold** was wrong. In the measuring pass the inner span read
|
||||||
|
`far = rel 1 - 17.6px` and wrote slots in it; in the placing pass the pad
|
||||||
|
got a narrowed frame of the same pixel length, was reused (its contract
|
||||||
|
had no pin, because `in_parent`'s `Part::Of` arm drops a child's
|
||||||
|
`extent_len`), and its child was re-expressed into a frame where `rel 1`
|
||||||
|
means 17.6 px less. The inner span's own record had the pin; only the
|
||||||
|
composition lost it. Composing the pin through `Of` -- exactly where the
|
||||||
|
part is the whole box less pixels, `pinned - part.px`, and pinning the
|
||||||
|
parent's own length otherwise -- fixed all six; the planner's earlier
|
||||||
|
version of the same experiment broke seeds under the old protocol because
|
||||||
|
the second draw was still there.
|
||||||
|
- Cost after the fix (widget draws / distinct widgets, depth 8): seed 1
|
||||||
|
cold 331/288, many 78/78, size 3/3, scroll 1, resize 40/15; seed 13 cold
|
||||||
|
1179/982, many 335/333, resize nothing drawn. The table with the baselines
|
||||||
|
is in the handoff. `many` at seed 13 touches 333 distinct widgets because
|
||||||
|
a share child is asked twice per span draw, and `resize` at seed 1 draws
|
||||||
|
40 because positive-direction spans without shares pin `far`; both are
|
||||||
|
step 4 there.
|
||||||
|
- The 1000-seed depth-6 oracle and the 2000-seed depth-4 scan were started
|
||||||
|
at `3091fb8` and had not finished when this was written; the next session
|
||||||
|
reads `/tmp/oracle-1000-6.log` and `/tmp/shrink-2000-4.log` if they still
|
||||||
|
exist, or reruns them.
|
||||||
|
|
||||||
|
## What the step 3 plan got wrong (planner, 2026-09-18, third session)
|
||||||
|
|
||||||
|
Three things, each a different kind of mistake:
|
||||||
|
|
||||||
|
1. **It kept the question the machinery could not answer.** The offer bit
|
||||||
|
existed because a widget was drawn twice, once where asked and once where
|
||||||
|
its answer placed it, and something had to say which draw was the
|
||||||
|
measurement. Both plans of 2026-09-18 tried to define that bit better
|
||||||
|
(the worker's four rules, the planner's two experiments). The repair is
|
||||||
|
to have one draw, after which the bit has nothing to name. A plan that
|
||||||
|
proposes bookkeeping for a distinction should first ask whether the
|
||||||
|
distinction has to exist.
|
||||||
|
2. **It stated the contract at the wrong box.** "Every drawing must hold for
|
||||||
|
the answer box it supplies" reads as a tightening and is actually
|
||||||
|
unsatisfiable for any widget that reads its box. The right statement is
|
||||||
|
the opposite: the answer box is never a question, so nothing is demanded
|
||||||
|
of the drawing there; `Holds` is about the ask box only.
|
||||||
|
3. **It narrowed frames by region.** The worker's share frames were
|
||||||
|
`extent_part(axis, From(span))`, a position in the span's frame; a frame
|
||||||
|
given as a position does not move when its part moves, and `re_ask` had
|
||||||
|
no way to put it back. A narrowed frame must be a *length* of the
|
||||||
|
parent's frame, placed into the part by alignment on every placement,
|
||||||
|
exactly as a declared rule already was.
|
||||||
|
|
||||||
|
The requirements did change under the plans (transparent frames, then
|
||||||
|
shares narrowing frames, then `Scroll`'s content length), which explains
|
||||||
|
some churn but not the three above; those were reasoning errors that a
|
||||||
|
five-widget reproduction with the retained records printed would have
|
||||||
|
caught in minutes. Print the records before proposing the next rule.
|
||||||
|
|
||||||
|
## The two shrinker seeds that were open on `wip/transparent-frames` (planner, 2026-09-18) -- closed by `wip/one-ask`
|
||||||
|
|
||||||
|
Both were the rule in `draw_inner` that decided which answer places a widget's
|
||||||
box: `measured = if info.offer() { fresh } else { retained }` with
|
box: `measured = if info.offer() { fresh } else { retained }` with
|
||||||
`offer() := place == offer_place`. That bit is meant to say "this ask is a
|
`offer() := place == offer_place`. That bit is meant to say "this ask is a
|
||||||
measurement" and has no consistent value once a container's body runs in
|
measurement" and has no consistent value once a container's body runs in
|
||||||
@@ -169,20 +251,24 @@ at depth 8; draw counts are deterministic so these are single runs.
|
|||||||
`e44dea3` is #18's head, `34cafb6` the commit the branch starts from,
|
`e44dea3` is #18's head, `34cafb6` the commit the branch starts from,
|
||||||
`49cec82` its head.
|
`49cec82` its head.
|
||||||
|
|
||||||
| seed 1 | e44dea3 | 34cafb6 | 49cec82 |
|
| seed 1 | e44dea3 | 34cafb6 | 49cec82 | 3091fb8 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| cold | 369/261/10.6 | 463/274/13.3 | 516/288/12.0 |
|
| cold | 369/261/10.6 | 463/274/13.3 | 516/288/12.0 | 331/288/12.3 |
|
||||||
| repaint | 1 | 1 | 1 |
|
| repaint | 1 | 1 | 1 | 1 |
|
||||||
| many | 157/95/0.33 | 263/108/0.59 | 187/119/0.52 |
|
| many | 157/95/0.33 | 263/108/0.59 | 187/119/0.52 | 78/78/0.13 |
|
||||||
| size | 16/12/0.018 | 3/3 | 3/3/0.010 |
|
| size | 16/12/0.018 | 3/3 | 3/3/0.010 | 3/3/0.008 |
|
||||||
| scroll | 2/0.002 | 1 | 1/0.004 |
|
| scroll | 2/0.002 | 1 | 1/0.004 | 1/0.001 |
|
||||||
| resize | 13/13/0.019 | 22/15/0.032 | 24/76/0.090 |
|
| resize | 13/13/0.019 | 22/15/0.032 | 24/76/0.090 | 40/15/0.096 |
|
||||||
|
|
||||||
| seed 13 | e44dea3 | 49cec82 |
|
| seed 13 | e44dea3 | 49cec82 | 3091fb8 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| cold | 1330/707/20.3 | 2940/982/28.3 |
|
| cold | 1330/707/20.3 | 2940/982/28.3 | 1179/982/24.6 |
|
||||||
| many | 524/159/1.09 | 1091/423/2.39 |
|
| many | 524/159/1.09 | 1091/423/2.39 | 335/333/0.62 |
|
||||||
| resize | nothing drawn | 2215/510/6.56 |
|
| resize | nothing drawn | 2215/510/6.56 | nothing drawn |
|
||||||
|
|
||||||
|
The `3091fb8` column is single runs from the third session; its `cold` and
|
||||||
|
`resize` at seed 1 are after the `Part::Of` pin composition, which added
|
||||||
|
18 and 19 draws respectively over the version without it.
|
||||||
|
|
||||||
Three findings, each applied on the branch:
|
Three findings, each applied on the branch:
|
||||||
|
|
||||||
@@ -268,7 +354,11 @@ is the padded share and fits. The clipped render records the current bug.
|
|||||||
`unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`.
|
`unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`.
|
||||||
- **Four bookkeeping rules for "which draw is a measurement"** (worker) and
|
- **Four bookkeeping rules for "which draw is a measurement"** (worker) and
|
||||||
**the two experiments above** (planner): each fixes some seeds and breaks
|
**the two experiments above** (planner): each fixes some seeds and breaks
|
||||||
others. The bit is undefinable; stop trying to define it.
|
others. The bit is undefinable; `wip/one-ask` removes the second draw, so
|
||||||
|
there is nothing left for it to name.
|
||||||
|
- **Asserting that a placed drawing holds for its answer box** (worker's
|
||||||
|
step 3): unsatisfiable for `Text` and for any widget that reads its box
|
||||||
|
and reports something else. The answer box is not a question.
|
||||||
- **Choosing between a fixed and a relative child in pixels at the span's
|
- **Choosing between a fixed and a relative child in pixels at the span's
|
||||||
current width** admits multiple self-sizing fixed points; seed 13 settled
|
current width** admits multiple self-sizing fixed points; seed 13 settled
|
||||||
differently warm and cold under it. The same circularity is what a cap
|
differently warm and cold under it. The same circularity is what a cap
|
||||||
|
|||||||
Reference in new issue
Block a user