Skip to content

fix(layout): evaluate (when FORM . SPEC) display specs and measure buffer :align-to from the text area - #354

Open
tag-und-nacht wants to merge 12 commits into
eval-exec:mainfrom
tag-und-nacht:fix/align-to-when-eval-and-text-area-origin
Open

fix(layout): evaluate (when FORM . SPEC) display specs and measure buffer :align-to from the text area#354
tag-und-nacht wants to merge 12 commits into
eval-exec:mainfrom
tag-und-nacht:fix/align-to-when-eval-and-text-area-origin

Conversation

@tag-und-nacht

Copy link
Copy Markdown
Contributor

Dashboard's centered banner (and any buffer text using (space :align-to REGION) or a (when FORM . SPEC) display spec) lands where GNU Emacs 31.0.90 puts it. Two defects, both visible on the dashboard logo:

  1. :align-to region targets were measured from the window edge, not the text area. center/left/right in buffer text came out one left-fringe width too far left (8px on a default frame). GNU produce_stretch_glyph makes a resolved region coordinate text-area-relative for buffer text and only then subtracts the pen (src/xdisp.c:32853-32859); chrome rows keep window coordinates and are unchanged.
  2. (when FORM . SPEC) was applied whenever FORM was non-nil. Dashboard's line-prefix carries (when (display-graphic-p) …) and (when (not (display-graphic-p)) …); a graphic frame took the text-terminal clause (42px off with a 17px font). GNU takes nil/t literally and otherwise evaluates FORM through dsafe_eval with object, position and buffer-position bound, an error counting as nil (src/xdisp.c:6130-6160); a list of specs stops at the first replacing element for strings and lets later ones override for buffer text (6034-6040, 6055-6061).

How the when forms are evaluated

The layout walk cannot run Lisp while it holds the buffer, so the forms of the span the walk can reach (the one fontification just covered) are evaluated once before it:

  • Context::display_when_form_holds (neovm-core, display/display_when/) is the dsafe_eval port: literal nil/t, inhibit-redisplay/inhibit-quit/inhibit-debugger bound like safe_funcall, the three GNU bindings, signals muted to nil, other flows propagated. Context::evaluate_display_when_sites roots every site's form and object (precise collector) and runs with the window's buffer current.
  • evaluate_window_display_when_forms (layout engine, display_when.rs) collects the sites: display/line-prefix/wrap-prefix text and overlay properties, the buffer-local and default prefix values, the display properties inside prefix strings, and overlay before/after-strings placed by the walk's own resolver (RustTextPropAccess::overlay_strings_at, so the window filter, the invisible-text rule and the load position have one owner). The scan is closed at its end so strings anchored at point-max are reached (GNU next_element_from_buffer, src/xdisp.c:9846-9860).
  • Results ride in LayoutBufferSnapshot and reach every reader through LayoutBufferView::layout_display_when_conditions: the text cursor, the row route, the bridge predicate, and the Lisp-string sources of buffer-scoped sessions. classify_display_property takes the conditions and the object kind (string vs buffer first/last rule).

Under line numbers both :align-to resolvers hand the pixel calculator GNU's numbers (whole box width plus the field) and measure from the text-area edge, so a numeric target and center both move by the whole field as in 31.0.90 (src/xdisp.c:30438-30441, 30493). Emacs master's half-field center is declared in comments and tests, not ported.

Declared gaps (in code comments)

  • A FORM is evaluated once per equal form with its first occurrence's bindings; GNU evaluates every occurrence.
  • The span end is the fontification budget, so boundaries past the last displayed row are evaluated where GNU would not reach them.
  • GNU's single-winner rule for overlay properties (src/textprop.c:656-675) is not applied; every covering overlay's form is evaluated.
  • Prefixes are read at the overlay start, GNU reads them at each row start (src/xdisp.c:25131-25132, 25180).
  • Frame-local chrome/margin strings and readers without a snapshot keep the structural rule (non-nil holds), as does a FORM the scan did not see.
  • No inhibit-eval-during-redisplay; dsafe_eval_handler's "Error during redisplay" line, the nested-redisplay abort and backtrace-on-redisplay-error are not ported.
  • Hscroll/continuation pen terms and GNU's one-pixel stretch past the target (src/xdisp.c:32882-32883) are declared in the geometry path.

Verification

  • Tests, red first, for the evaluator bindings and error rule, the scan (text props, overlays, window-scoped overlays, hidden-overlay strings, point-max anchors, buffer-local prefixes), the classifier rules, the row builder (center with and without a fringe), and a realized GUI frame selecting the graphic clause when the text-terminal clause is listed first.
  • Rendered-surface readback on macOS: the centered dashboard image lands at the GNU x-coordinate at 1x, and where GNU's arithmetic puts it for the rendered image at 2x.
  • Six adversarial review rounds; every confirmed finding is fixed in the follow-up commits (rooting across Lisp, Flow propagation, the 31.0.90 vs master center arithmetic, overlay-string placement through the walk's resolver, after-string buffer-position at the overlay end, point-max anchors).
  • Linux and Windows not run; the branch has no platform-specific code.

Found while comparing with GNU but not in this PR: at 2x images auto-scale from the device column width and render smaller than GNU's, and the native macOS menu bar is not implemented (menus render in-frame on every platform).

🤖 Generated with Claude Code

https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC

@eval-exec
eval-exec requested review from eval-exec and a balanced review from Copilot September 6, 2026 15:44
@eval-exec eval-exec added this to the v0.0.18 milestone Sep 6, 2026
@tag-und-nacht

Copy link
Copy Markdown
Contributor Author

Reviewed at 86e193634108910704ccfd09aa38a0fb77d8287c against the commit history, conversation export, and GNU Emacs 31.0.90 source. I found four issues to fix before merging:

  1. [P1] Changed conditions do not invalidate retained rowsengine.rs:3554

    The engine chooses row reuse before evaluating conditions, then reuses those rows even when a condition changes. Reproduced: a true condition displays "REPLACED"; toggling it to nil evaluates the condition again but leaves "REPLACED" visible. A fresh engine correctly displays the underlying "X". The probe reported cursor_only_windows: 1 and reused_rows: 2. Condition results need to participate in reuse validity.

  2. [P2] A disabled occurrence suppresses an enabled occurrence elsewheredisplay_when/mod.rs:86

    Deduplication checks only the form, before checking eval_enabled. If (disable-eval (when (= 1 1) . "X")) precedes an ordinary (when (= 1 1) . "Y"), the disabled occurrence caches false for both. A layout probe using these properties on the two lines of a\nb\n displayed a and b, instead of a and Y. GNU applies the disabled-evaluation policy separately to each specification (xdisp.c:6139–6140). Disabled evaluation must remain specific to its occurrence.

  3. [P2] The scan executes unreachable string clausesdisplay_when.rs:270

    For a prefix whose display property is ("X" (when (progn (setq probe t) nil) . "Y")), GNU stops after the first replacement and never evaluates the condition (xdisp.c:6034–6040). This scan executes it anyway: a layout probe confirmed that probe becomes true. The classifier’s later short-circuit cannot undo these side effects; evaluation must honor the same stopping rule.

  4. [P2] Conditions see the wrong point in nonselected windowsdisplay_when/mod.rs:45

    Evaluation selects the window’s buffer but does not install its point. With two windows sharing a buffer, selected-window point 1 and other-window point 5, (when (= (point) 5) . "YES") incorrectly fails in the other window. The probe displayed abcdef there instead of YESbcdef. GNU temporarily installs that window’s point during redisplay (xdisp.c:20578 onward). The evaluation context needs equivalent setup and restoration.

Validation: 39 existing scoped tests passed, including alignment and conditional-prefix integration tests. Four additional temporary regression probes reproduced the findings above; all source edits were restored after the review. The full suite and GUI pixel checks were not rerun for this review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

tag-und-nacht and others added 12 commits September 6, 2026 10:53
…area

`(space :align-to center)` (and `left`/`right`) in buffer text came out one
left-fringe width too far left: the pixel calculator resolves the region
symbol in text-area coordinates (`text_area_left == 0` for buffer rows)
while `stretch_width` subtracted a pen measured from the row origin, the
text area's left edge in window coordinates.  Dashboard's centered banner
and title sat 8px left of center on a default frame.

GNU `produce_stretch_glyph` (src/xdisp.c:32853-32859, emacs-31.0.90) makes
a resolved region coordinate text-area-relative for buffer text,
`align_to - window_box_left_offset (it->w, TEXT_AREA)`, keeps window
coordinates for mode/header/tab lines (`else if (align_to < 0) align_to =
window_box_left_offset (...)`), and only then subtracts the pen.  Move the
target into the row's origin space for buffer rows; chrome rows have a
zero origin and window-coordinate targets already, so they are unchanged.

Test: a buffer row with `(space :align-to center)` in a 240px text area
yields a 120px stretch with a 0px and with an 8px fringe (was 112px with
the fringe).  Verified against the rendered surface: neomacs placed a
centered 156px image at x=162 where GNU places it at 170.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
A `(when FORM . SPEC)` display spec was applied whenever FORM was merely
non-nil ("resolved structurally"), and a list of specs kept its last
replacing element for strings as well as buffers.  Dashboard centers its
banner with a `line-prefix` whose `display` value is
`((when (display-graphic-p) space :align-to (- center (0.5 . IMAGE)))
  (when (not (display-graphic-p)) space :align-to (- center 15)))`, so a
graphic frame got the text-terminal clause: the logo sat 15 columns from
the center instead of half its width, 42px too far left with a 17px font.

GNU `handle_single_display_spec` (src/xdisp.c:6130-6160, emacs-31.0.90)
takes nil and t literally and otherwise evaluates FORM through
`dsafe_eval` with `object`, `position` and `buffer-position` bound
(`specbind`), an error counting as nil; `handle_display_spec` stops at
the first element that replaced text of a STRING and lets later elements
override for buffer text (`if (!it || STRINGP (object)) break;`,
src/xdisp.c:6034-6040 and 6055-6061).

The walk cannot run Lisp while it holds the buffer, so the forms of the
span the walk can reach -- the one fontification just covered -- are
evaluated once before it (`Context::display_when_form_holds`, the GNU
bindings and error rule; `evaluate_window_display_when_forms`, the scan of
`display`/`line-prefix`/`wrap-prefix` text and overlay properties, the
buffer-local prefix values, and the `display` properties inside prefix
strings).  The results ride in the `LayoutBufferSnapshot` and reach every
reader through `LayoutBufferView::layout_display_when_conditions`: the
text cursor, the row route, the bridge predicate, and the Lisp-string
sources of buffer-scoped sessions (prefixes, overlay strings, display
replacements).  `classify_display_property` takes the conditions and the
object kind, with the string-vs-buffer first/last rule.

Declared, not GNU: a FORM is evaluated once with its first occurrence's
bindings (GNU evaluates every occurrence); frame-local chrome strings and
readers without a snapshot keep the structural rule (non-nil holds); a
FORM the scan did not see falls back to it too.

Tests (red first): the evaluator binds the three variables, unbinds them,
and maps an error to nil; the scan evaluates forms from text properties,
overlays and the buffer-local prefix; the classifier honours a false
result and the first-replacing string rule; a realized GUI frame laying
out a prefix with the text-terminal clause listed FIRST selects the
graphic clause (column 10, not 20) -- red when the engine does not attach
the evaluated results.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
… line numbers

Adversarial review of the two previous commits (four blocking findings,
three confirmed against the mirror, the fourth confirmed on a different
code path than it named):

- `when` forms ran with the caller's buffer current.  GNU's iterator runs
  after `set_buffer_internal_1 (XBUFFER (w->contents))` (src/xdisp.c:
  20533-20535); `Context::with_display_buffer_current` now selects the
  window's buffer around the evaluation and restores the caller's, the
  same switch fontification uses.
- `display_when_form_holds` was a bare `eval`.  GNU `dsafe_eval` is
  `dsafe_calln (true, Qeval, sexpr, Qt)` (src/xdisp.c:3170-3173): it binds
  `inhibit-redisplay` and `inhibit-quit`, evaluates lexically, and maps an
  error to nil (3118-3131).  Ported with the `try_specbind_or_unwind_to` /
  `unbind_to_with_result` pattern the other `dsafe_call` sites use.
  Declared: no `inhibit-eval-during-redisplay` variable here, and the
  catch-all condition frame that keeps the debugger out is not ported
  (as for the other `dsafe_call` ports).
- `(disable-eval …)` pushed a nil FORM into the table, so the real FORM was
  never found and the spec applied.  GNU takes FORM as nil there
  (src/xdisp.c:6139-6140); the site now records the FORM as failing.
- Buffer-text `:align-to` under `display-line-numbers`.  Buffer `display`
  stretches resolve through `DisplaySpaceGeometry`, whose pixel context had
  no line-number width, so `center` was half the full text width where GNU
  puts it at the field plus half of the remainder (vanilla 31.0.90 oracle:
  28px field, 560px text area, `center` at 294px, `:align-to 10` at 98px).
  The context now carries the field and the width past it; a raw number
  is measured from the text-area edge with the field folded into it
  (`XFLOATINT (prop) * base_unit + lnum_pixel_width`, src/xdisp.c:30248;
  `align_to = 0`, 32853-32859).  The row builder, which serves prefix and
  overlay strings, counted the field's glyphs into the pen a second time;
  it now takes them out as GNU does (`x -= it->lnum_pixel_width`,
  src/xdisp.c:32846-32848) through the field width the row geometry
  carries.  Declared: the two resolvers remain separate implementations of
  one GNU rule; continuation-row and horizontal-scroll pen adjustments
  (src/xdisp.c:32841-32843, 32862-32874) are not ported.

Also from the review: the scan covers overlay `before-string`/
`after-string`s and the default `line-prefix`/`wrap-prefix` values; the
evaluation runs inside the freshness/topology guards; the conditions are
a closed `Structural | Evaluated` type with an explicit `Unseen` verdict;
the classifier's doc is back on the function; citations corrected.

Tests: evaluation binds and unbinds `inhibit-redisplay`/`inhibit-quit`;
the window's buffer is current and restored; a `disable-eval` form fails;
a single false clause leaves the prefix out (red on the old structural
rule regardless of the first/last rule); under `display-line-numbers` a
numeric target moves by the whole field and `center` by half of it for
buffer text and for prefix strings alike (red before on both paths).
Verified on macOS; Linux and Windows not run (no platform-specific code).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
…ign-to`

Adversarial review of the three previous commits (three blocking findings,
all confirmed):

- The `center` arithmetic ported was Emacs master's, not the 31.0.90
  baseline: the "vanilla oracle" on this machine is a 32.0.50 build, whose
  `center` arm is `(LNUM_ONCE + window_box_width) / 2`, while 31.0.90 has
  `window_box_left_offset + lnum_pixel_width + window_box_width / 2`
  (src/xdisp.c:30438-30441) with no line-number term in
  `window_box_width` (src/xdisp.c:1279-1300).  Shrinking the context's
  text width to reproduce master also double-subtracted the field in the
  `text` arm (src/xdisp.c:30418-30421).  Both resolvers now hand the
  calculator GNU's numbers -- the whole box width plus the field -- and
  measure from the text-area edge: the geometry path keeps
  `params.text_bounds.width`; the row builder's context is the append
  width plus the field with `line_number_pixel_width` set, and buffer
  rows base both raw and region targets on `content_x -
  line_number_width`.  Under line numbers a numeric target and `center`
  therefore both move by the whole field (31.0.90); master's half-field
  `center` is declared in the comments and tests.
- The site values were held in Rust locals across Lisp evaluation.  The
  collector is precise (tagged/CONCURRENT_GC.md); a FORM that drops the
  property carrying a later FORM would free it.  The evaluation loop now
  lives in neovm-core (`Context::evaluate_display_when_sites`), roots every
  site's form and object through `save_specpdl_roots`/`push_specpdl_root`
  for the loop's span, and runs with the window's buffer current.
- `display_when_form_holds` muted every `Flow`.  GNU's
  `internal_condition_case_n` catches conditions only; the port's
  `safe_funcall` mutes `Flow::Signal` and propagates the rest.  It now
  returns `Result<bool, Flow>`, mutes signals to `Ok(false)` with
  `inhibit-debugger` bound like `safe_funcall`, and a non-signal flow makes
  the engine fall back to the structural rule for that layout (declared:
  redisplay here cannot unwind a `throw`).

Also: an overlay `after-string` binds `buffer-position` to the overlay's
end (GNU `it->current.pos`, src/xdisp.c:5919-5923); the `XFLOATINT (prop)
* base_unit + lnum_pixel_width` citation is src/xdisp.c:30493 (the
previous commit said 30248); `dsafe_eval_handler`'s "Error during
redisplay" `*Messages*` line (src/xdisp.c:3098-3104) is declared not
ported; `with_display_buffer_current` documents the killed-caller case.

Tests: the site loop evaluates each form once with the buffer current and
survives a `garbage-collect` inside a form; the line-number engine test
now expects the 31.0.90 whole-field shift for `center` on both paths.
Verified on macOS; Linux and Windows not run (no platform-specific code).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
Adversarial review of the previous commit found its after-string claim
was not in the diff: an overlay `after-string`'s `buffer-position` was
still the overlay's start.  GNU loads the after-string when the iterator
reaches the overlay's end (src/xdisp.c:7172-7173) and binds
`buffer-position` to that position (src/xdisp.c:5919-5923); the site now
uses the overlay end, the before-string and prefixes keep the start.

Also from the review: the scan skips overlays scoped to another window
before it records their strings, as GNU does ("Skip this overlay if it
doesn't apply to IT->w", src/xdisp.c:7153-7156; the window id now reaches
the scan); the once-per-form rule is stated as once per `equal` form (the
table is keyed structurally); a before-string of an overlay starting
before the span is declared bound to the span start; the `dsafe__call`
pieces not ported are listed (nested-redisplay abort, src/xdisp.c:
3127-3136; `backtrace-on-redisplay-error`, 3159-3160); the geometry path
declares the continuation/hscroll pen terms and GNU's one-pixel stretch
past the target (src/xdisp.c:32882-32883); a coordinate comment now says
frame-absolute; the engine's fallback log no longer calls a dead buffer a
non-error flow.

Tests: an after-string's form sees the overlay end and a before-string's
the start; an overlay with a `window` property is scanned only for that
window.  Verified on macOS at 2x (numeric and image-based targets land
where GNU's arithmetic puts them for the rendered image); Linux and
Windows not run (no platform-specific code).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
…hen` scan

Adversarial review of the previous commit: the scan computed for itself
where an overlay's before- and after-strings are displayed, and drifted
from the walk on two GNU rules -- an overlay whose text is invisible
shows both strings at whichever end the walk reaches (src/xdisp.c:
7158-7175), and the strings of an overlay that starts or ends exactly at
the iterator position are loaded there (src/xdisp.c:7141-7150), so an
overlay ending at the window start still shows its after-string on the
first row.  The scan now collects the overlay boundaries inside the span
and asks `RustTextPropAccess::overlay_strings_at`, the walk's own
resolver, which strings are displayed at each, binding
`buffer-position` to that position (`it->current.pos`, src/xdisp.c:
5919-5923); the `window` filter, the invisible-text rule and the load
window have one owner.  Strings displayed at a boundary outside the span
are not evaluated (declared).  The overlay `display`/prefix properties
keep the overlay loop with the same `window` filter, which GNU applies to
properties through `overlay_matches_window` as well as to strings.

Also from the review: the geometry path no longer declares a phantom
gap -- GNU's `zero_width_ok_p` (src/xdisp.c:32860, 32876) gives a
past-target `:align-to` zero width, as here -- and the overlay-prefix
comment declares that GNU reads a prefix at each row start
(src/xdisp.c:25131-25132, 25180) where the scan binds the overlay start.

Tests: a hidden overlay's after-string sees the overlay start; an
overlay ending at the window start is scanned (its nil form fails) while
one ending past the span stays unseen.  Linux and Windows not run (no
platform-specific code).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
Adversarial review of the previous commit: the scan kept only overlay
boundaries strictly before the span end, so strings anchored at
point-max -- a completion popup's empty overlay with its candidates in
`before-string`, or an overlay ending at point-max with an after-string
-- were never evaluated although the walk displays them through its
end-of-buffer anchor path (GNU `reseat` -> `handle_stop` at ZV,
src/xdisp.c:8046-8052).  The span is closed at its end now.

Also from the review: the overlay `display`/prefix membership is decided
from the overlay's own start and end instead of a per-overlay range
query compared structurally; an empty overlay contributes no property,
as GNU `get_char_property_and_overlay` skips an overlay whose end is at
or before the position (`node->end < pos + 1`, src/textprop.c:652-653);
the accessor is built with `new_for_optional_window`.

Tests: an empty overlay at point-max and an overlay ending there have
their strings' forms evaluated; the beyond-span case ends past the span.
Linux and Windows not run (no platform-specific code); clippy clean on
the changed file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
…re scan bounds

Upstream's architecture tests now require every `emacs_core/<domain>`
entry to be a subsystem directory and out-of-line tests to live under a
`tests/` directory; the two loose files this branch added under
`emacs_core/display/` were the only remaining violators after the
rebase.  Move them to `display/display_when/mod.rs` and
`display/display_when/tests/mod.rs`.

From the sixth review of the scan: the span end is declared -- a `to`
short of the buffer end is the fontification budget, so boundaries and
rows past the last displayed one are evaluated where GNU would not reach
them; the strings anchored at ZV are loaded by `next_element_from_buffer`
(src/xdisp.c:9846-9860), which the comment now cites instead of
`reseat`; and GNU's single-winner rule for overlay properties
(src/textprop.c:656-675) is declared, the scan evaluating every covering
overlay's form.  Test: an empty overlay's `display` form stays unseen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxL1hNkQvGiUyXAHJJuyXC
Rebuild window bodies with evaluated when forms instead of replaying glyphs
whose arbitrary Lisp dependencies are absent from the retained key. Keep
disable-eval local to each property so equal enabled forms remain independent.

Preserve property clause order during evaluation and use the layout
classifier to stop string evaluation at the first replacing clause. Root
saved elements and freshly decoded forms/payloads across mutation and GC.
Temporarily install nonselected window point using an unwind-protected marker.

Add ten regression tests with red/green verification for the four findings,
condition ordering, point restoration on error, and both detached-payload GC
cases. Correct the documented GNU buffer replacement limitation.

Validation: targeted layout/core tests and formatting pass; fresh review
found no remaining issues. Clippy passes with the pre-existing never_loop
lint allowed. Full-suite validation remains inconclusive: the unchanged PR
head had 55 failures, and modified full runs aborted with a segmentation
fault and stack overflow. Individually checked additional failures pass on
both revisions.
Share imagep validation with the display classifier so an invalid or
unsupported image does not suppress later conditional string clauses.
Update image classification fixtures to use valid descriptors.

Save the nonselected window buffer's numeric character position and
resolve its byte position after evaluation, including errors and throws.
GNU redisplay restores that numeric position rather than a marker that
moves when Lisp inserts text before point.

Reproduced both failures before implementation. Add six engine tests for
invalid, unsupported and valid images, plus multibyte edits with normal,
signal and throw outcomes. Conditional display tests (33), display
property tests (53), and image tests (110) pass. Formatting and Clippy
pass with the existing never_loop lint allowed. Earlier full-suite
validation remains inconclusive as recorded in the preceding commit.
GNU handles one WHEN, then one optional margin prefix, before checking
ordinary replacement content. Recursive classification could accept a
nested wrapper and newly skip a later condition in a display string.

Separate classification of resolved payloads from conditional unwrapping
and use it directly after evaluation. Restrict margin contents to ordinary
replacement kinds so nested margins or WHENs cannot reenter decoding.

Reproduced three failing engine cases before changing production code:
nested WHEN, WHEN inside a margin, and nested margins. All now evaluate
the following condition and display its replacement. Conditional tests
(36) and display-property tests (53) pass, as do formatting and Clippy
with the pre-existing never_loop lint allowed.
Return an empty classification immediately for resolved WHEN wrappers
and invalid images. Falling through after rejecting their replacement
could otherwise reinterpret embedded :raise or :height keywords as text
modifiers and alter the original text.

Both regression tests failed with nonempty modifiers before the fix.
Display-property tests (55) and conditional-display tests (36) pass;
formatting and Clippy pass with the existing never_loop lint allowed.
Linux, Windows, and manual GUI validation were not run. Earlier full-suite
validation remains inconclusive as documented in the first fix commit.
@tag-und-nacht
tag-und-nacht force-pushed the fix/align-to-when-eval-and-text-area-origin branch from 7ac86d3 to acd5feb Compare September 6, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants