The LZW data for both GIF and TIFF images is sometimes intentionally
missing an end-of-information (EOI) code, which technically is a
decoding error, but in practive is handled gracefully by Firefox, Safari
and Chrome for GIFs and Safari for TIFFs. Let's mirror their behavior.
The included WPT test exposes the fact that trailing garbage bytes can
also result in decoding errors. We handle this in the LZW logic rather
than in the image decoding since our LZW implementation is currently
only used by GIF and TIFF decoding. The error is logged behind the
LZW_DEBUG flag.
Add broader style-invalidation regression coverage for selector shapes
that are easy to mis-model when invalidation data is flattened into
feature sets. Cover selector-list alternatives, nested pseudo-class
arguments, pseudo-element arguments, nth-child selector lists,
quirks-mode class matching, mixed-case ancestor filters, duplicate
invalidation rule merging, and concrete :has() feature filtering.
Keep the expectations on the unoptimized baseline so follow-up
optimization commits can show the counter progressions separately.
These tests dump the normal invalidation counters instead of asserting
specific counter values in script.
To do this we parse the grammar supplied in `ValueTypes.json` and
generate the appropriate parsing function in `CSS::Parser::Parser`
Only a small subset of the CSS grammar (i.e. types `<foo>` and
alternatives `<foo> | <bar>`) is implemented so far, it will be
expanded in later commits.
When an inline-relative is split by block-level descendants, the rect
computation only looked at one anonymous wrapper and returned empty
for everything else, sending abspos placement back to the initial
containing block. On Reddit this let an inline-relative ad host's <a>
overlay the entire viewport and steal clicks from the post gallery's
pager buttons.
Walk every descendant of the inline's real block container instead,
collecting fragments from any InlineNode part of the inline plus the
border-box rects of its in-flow Box descendants. Matches what other
engines expose via getClientRects() for split inlines.
While here, narrow the inline-CB detection to the triggers that
actually apply on non-atomic inlines: `position`, `filter`,
`backdrop-filter` (and their will-change hints). transform, contain
and the rest don't apply per their specs - the broader check would
have started routing the WPT contain-paint/contain-layout ib-split
tests through the inline path once the rect computation began
returning non-empty results.
An `inset: 0` abspos child of an inline-relative with block-level
descendants should size to the inline's full extent across the
"before"/"middle"/"after" wrappers. The expected output records the
current buggy state (most cases collapse to the viewport); the next
commit fixes it.
Per "find flattened slotables", a <slot> whose root is a shadow root is
recursed through, not appended to the result. ::slotted() in an outer
shadow must therefore not match such an intermediate slot.
Fixes the gallery on Reddit comment pages: a re-slotted <slot> was
picking up `::slotted(:not([slot])) { display: grid }` from the inner
shadow, which made the <ul> size to its content rather than the flex
container, leaving the carousel's "next" button with a 0px translate.
::slotted() rules from an outer shadow currently leak onto intermediate
<slot> elements re-slotted into that shadow. The expected output records
the buggy state; the next commit fixes it.
When an insert or remove flips a sibling's match for :first-child,
:last-child, :nth-child, :nth-of-type, or a + / ~ combinator, only the
sibling's own match changes. Descendants only need rechecking when a
selector ties them to the sibling's position.
invalidate_structurally_affected_siblings used to mark the affected
sibling's entire subtree, so a sibling with N descendants turned a
match-state flip into N+1 element recomputes. Reuse the per-element
flags introduced for the same-parent-move optimization: when neither
affected_by_structural_pseudo_class_in_non_subject_position nor
affected_by_sibling_combinator_in_non_subject_position is set, mark
only the sibling root, and let the existing inherited-update path
reach descendants if the sibling's own style changes.
Rebaseline the impacted tests. styleInvalidations gains one per
affected sibling because set_needs_style_update increments the counter
(entire-subtree marks did not), but elementStyleRecomputations falls
accordingly.
Pin the recompute counters (and shadow-DOM correctness) when a
structural mutation flips a sibling's match for :first-child,
:last-child, :nth-child, :nth-of-type, or a + / ~ combinator. The
affected sibling's entire subtree is marked dirty today, so an 8-leaf
sibling produces elementStyleRecomputations=11. Shadow-host cases pin
that inherited-style propagation reaches light DOM, light-DOM-hosted
shadow roots, and nested shadow DOM under the affected sibling.
The next commit replaces the entire-subtree mark with a root-only mark
when no descendant selector ties matching to the sibling's position,
and rebaselines the counter expectations.
Introduce IncrementalDocumentParser, which streams the response body
through a TextCodec::StreamingDecoder into the HTMLTokenizer one chunk
at a time. The tokenizer pauses when it runs out of input and resumes
once the next chunk is appended; when the body closes we close the
tokenizer's input stream so it can finish the parse.
DocumentLoading routes HTML responses through the new parser instead of
buffering the full body before handing it to HTMLParser.
Add a ScriptCreatedParser flag plumbed through HTMLParser's constructor
and create_for_scripting(). Only document.open()'s parser sets it to
Yes. Document::close() step 3 now checks is_script_created() so it
correctly skips parsers that weren't created via document.open(),
matching the spec.
Previously the check was just `if (!m_parser)`, which incorrectly let
document.close() insert an EOF into a network-driven parser. The bug
was mostly latent because the network parser used to finish quickly,
but it matters once the network parser stays alive for the duration of
a streamed parse.
Introduce a StreamingDecoder wrapper that lets callers feed bytes to a
Decoder one chunk at a time. It buffers any incomplete trailing byte
sequence at the end of a chunk and prepends it to the next chunk, so a
multi-byte code point split across a chunk boundary is decoded correctly
once the next chunk arrives.
To support that, add an incomplete_tail_length() virtual on Decoder
returning the number of trailing bytes that form an incomplete sequence
per the Encoding Standard's decoder handler byte ranges, with overrides
for UTF-8, UTF-16BE, UTF-16LE, GB18030, Big5, EUC-JP, ISO-2022-JP,
Shift_JIS, and EUC-KR. The default implementation returns 0, which keeps
single-byte legacy decoders correct.
This is the foundation for the upcoming incremental HTML parser, which
needs to decode network response bodies as they arrive.
We were forming parsed JSON strings as a ByteString and converting this
to a String wrapped with a MUST. Let's instead create a String from the
get-go and let the encoding error propagate.
This is the streaming counterpart to TextDecoder, used by sites that
process responses with `pipeThrough(new TextDecoderStream())` or that
otherwise consume a Response body as decoded text.
The transform algorithm appends each chunk's bytes to the I/O queue
and decodes them via the underlying LibTextCodec decoder, holding back
any trailing partial UTF-8 sequence so chunk boundaries don't produce
spurious replacement characters. The flush algorithm emits a single
replacement character if a partial sequence was still pending. For
non-UTF-8 encodings the underlying decoders are stateless across
calls, so each chunk is decoded in full with nothing carried over.
CSSStyleSheet::evaluate_media_queries previously flagged "no recorded
result yet" as a match-state change, so every freshly-loaded sheet
fired MediaQueryChangedMatchState on the first pass through
Document::evaluate_media_rules. For sheets added through
adoptedStyleSheets that piled an extra full-document style invalidation
on top of the AdoptedStyleSheetsList one, recomputing every element a
second time for nothing.
Drop the !has_value() leg so the very first evaluation establishes the
baseline silently. The sheet's rules already entered the cascade through
StyleSheetListAddSheet, AdoptedStyleSheetsList, or invalidate_owners,
each of which performs its own targeted invalidation.
Two callers relied on the implicit "first eval forces a refresh"
behavior to handle freshly-mutated state:
- invalidate_owners resets m_did_match, then leans on the next eval to
repopulate it. With the new semantics it must also re-evaluate the
sheet eagerly so MediaList::matches() and inner @media state are
fresh before the next rule cache build reads them.
- The adoptedStyleSheets on_set callback didn't evaluate at all,
relying on Document::evaluate_media_rules to populate
MediaList::m_matches. That worked accidentally because the false
flip retriggered invalidate_rule_cache after the matches had been
populated. Mirror StyleSheetList::add_sheet by evaluating the sheet
at adopt time so the rule cache build sees the correct match state
even if it runs first (e.g. via a :has() invalidation pass).
Add focused coverage for the counters produced when a stylesheet's media
queries are evaluated for the first time. Two paths are exercised:
- document.adoptedStyleSheets, where the sheet is added without an
eager evaluate_media_queries call. The next update_style currently
flips m_did_match from "no recorded result" to a recorded one and
fires MediaQueryChangedMatchState on top of the AdoptedStyleSheetsList
invalidation, so fullStyleInvalidations bumps twice.
- <style> element add via StyleSheetList::add_sheet, which evaluates
media queries eagerly and absorbs the first-eval flip. These tests
pin that behavior so future changes don't regress it.
Cases vary the outer MediaList (empty, all, screen, print, width-based
matching and not), the rule selector (matching, non-matching), inner
@media rules (matching, non-matching, mixed, nested), and @supports
wrapping inner @media. The expectations record behavior before the
optimization, so subsequent commits can show counter progressions.
Before applying an effect context (filter, opacity, blend-mode), the
display list player runs a culling check by tentatively switching to the
effect's parent first. When the effect was already on the stack (i.e.
the current context was a descendant of the target) that walk restored
the saveLayer prematurely. The filter was then applied to a partial
batch of commands, and subsequent commands opened a fresh saveLayer for
a second, independent application, producing visibly wrong compositing.
Fold the culling check into switch_to_context: while walking down to
apply contexts, check the bounding rect right before each EffectsData
node and abort if the command is fully clipped. Walks that only go up
never traverse an EffectsData node, so an already-applied effect is
never torn down.
This regressed in cd0705334b.
When a :has() mutation is known to come from a specific subtree, use
that subtree as the mutation root while walking observed ancestors.
Before dirtying an anchor and its non-subject descendants, check whether
any cached :has() rule for that anchor can observe the changed subtree.
This keeps unrelated descendant mutations from invalidating every rule
that merely contains :has().
Moving a node within the same parent changes sibling and positional
relationships, but it does not make every descendant of the moved node
need a fresh computed style. Handle this as a structural mutation at
the old and new sibling edges and dirty only the moved root and the
affected ancestors, instead of marking the entire moved subtree.
Factor the existing previous- and next-sibling structural invalidation
out of Node::invalidate_style() into invalidate_structurally_affected_-
siblings(), and pull the ancestor child-needs-style-update walk into
mark_ancestors_as_having_child_needing_style_update(). The new
invalidate_style_after_same_parent_move() reuses both helpers.
Whether the moved root itself needs its own style recomputed depends
on whether any selector matched against it (or against a descendant)
relied on its position in the sibling list. Track that via two new
sticky bits on Element, set during selector matching:
- m_affected_by_structural_pseudo_class_in_non_subject_position
- m_affected_by_sibling_combinator_in_non_subject_position
Both are write-once (sticky) because matching descendants can set them
while we're not currently re-matching this element's own selectors;
keeping them set is conservative and avoids stale descendant style.
When neither bit is set and the element only carries subject-position
positional/sibling/has() involvement, we just dirty the root and skip
its descendants.
Rebaseline same-parent-move-root-only and the structural-feature filter
counters to reflect the new path. Matching behavior is unchanged.
Element::includes_properties_from_invalidation_set() previously
short-circuited and returned true for the id and class attributes,
because the parsed id and class-name state was treated as the source of
truth and the attribute presence check could disagree with it. That
shortcut over-invalidated for any [class] or [id] selector even when
neither attribute had ever been touched on the element.
Drop the special case and answer attribute presence queries the same
way for every attribute: the element either currently has the attribute,
or has had it removed earlier in this style update batch.
To handle the just-removed case, track the set of attribute names that
were removed since the last invalidation pass on a new per-Element
Vector<FlyString, 1> m_removed_attributes_for_style_invalidation. The
StyleInvalidator clears the vector for each element it visits during
perform_pending_style_invalidations, so it only ever holds names from
the current batch.
Rebaseline the affected attribute-presence tests; counters drop because
elements that never had id/class no longer match [class] / [id]
invalidations.
Track the simple selector features that appear inside :has() arguments
on each StyleScope, then consult that metadata before scheduling an
ancestor walk for a structural mutation. If the mutated subtree has no
tag, id, class, attribute, or pseudo-class feature that any cached
:has() argument cares about, skip the walk entirely.
Stay conservative for featureless-sensitive arguments such as :has(*),
:has(:not(...)), :has(:empty), and child-index pseudos: an unfeatured
node can still start or stop matching there. Track that case via a new
has_selectors_sensitive_to_featureless_subtree_changes flag on
StyleInvalidationData and fall back to the old conservative walk.
Stay conservative for pseudo-classes the subtree filter cannot probe
(:focus, :hover, validation pseudos). Move :default out of the set of
trackable feature pseudo-classes for the same reason; it now triggers
the conservative walk where it previously recorded metadata.
Tag and attribute names are stored lowercased, so for non-HTML elements
(SVG, MathML) treat lowercased matches as scheduling hints only; the
actual :has() match still goes through case-sensitive selector matching.
Test counter expectations are rebaselined to reflect the skipped walks
and reduced recomputations. Matching behavior is unchanged.
Add focused coverage for style invalidation matrix behavior.
Cover concrete invalidation metadata collection, :defined triggers,
same-parent moves, :has() mutation roots, and feature filtering.
Also cover dynamic and unprobeable pseudo-classes inside :has().
These include mixed-metadata cases where another :has() selector in the
same scope records concrete metadata.
Add SVG and MathML case-sensitive name coverage for :has() filters.
The expectations record behavior before the optimization, so the next
commit can show counter progressions clearly.
Share the style cache for shadow roots whose only active author sheet is
the same constructed stylesheet. Matching already carries the effective
shadow root separately, so the cache can be reused while selectors such
as :host and ::slotted() still evaluate against each consuming shadow
root.
Keep the optimization conservative by falling back to the existing
per-scope cache whenever the shadow root has multiple active sheets, a
non-constructed sheet, or a page user stylesheet. Drop the shared cache
when the stylesheet rules or media query match state change.
Add coverage for two shadow roots adopting the same constructed sheet,
including :host, ::slotted(), and replaceSync() invalidation.
CSSOM declaration mutations on style rules and nested declarations
do not change selector buckets, layer ordering, or keyframe sets stored
in rule caches. Keep those caches valid and only invalidate affected
styles, while leaving keyframe declaration mutations on the existing
cache-invalidating path.
Add coverage showing a CSSOM style declaration mutation is observed
through an already-built rule cache.
Invalidate only the style scope whose media rules changed instead
of throwing away every shadow root rule cache whenever any active
stylesheet changes media query match state. Shadow-root stylesheet
changes still dirty the host side because :host and ::slotted
selectors can affect nodes outside the shadow tree.
When scoped invalidation leaves dirty descendants in a shadow root,
preserve the host ancestor chain so the document style update walk
reaches them before forced layout.
Add coverage that a matching media rule introduced in one shadow tree
does not broadly invalidate a page full of unrelated shadow roots,
and that a dirty shadow root is updated before layout is forced.
CSSFontFaceRule inherited from CSSStyleSheet::Subresource, which made
each face a critical subresource of its parent stylesheet. New
subresources start in the Unloaded state, and the stylesheet's
loading_state() treats Unloaded as Loading.
When a @font-face declares a unicode-range and no codepoint in that
range is ever rendered, FontComputer only registers the face for
matching and never calls FontFace::load() on it. The face stays
Unloaded, so the parent stylesheet stays stuck reporting Loading,
which keeps HTMLLinkElement's load-event delayer alive and prevents
the document load event from firing. HTMLParserEndState then times
out in phase 2 (WaitingForLoadEventDelay) after 15 seconds.
Decouple @font-face from the stylesheet's loading state. Font loading
remains tracked by FontFaceSet, which is the correct place.
HTML newline normalization collapses CRLF into a single LF, so
next_code_point() needs one code point of lookahead at a CR to decide
whether the CR stands alone or is the first half of a CRLF pair. When
the tokenizer is paused at the insertion point and the next code point
to consume is a CR sitting one position before it, that lookahead has
not been written yet.
Previously the tokenizer consumed the CR and emitted it as LF, so a
subsequent document.write() that began with LF surfaced as a second
LF instead of being absorbed into the original CRLF pair.
Stop one code point earlier in this case and wait for the next write
to arrive. This makes four html5lib write_single WPT tests pass.
Fixes flakiness in worker tests that create a Worker or SharedWorker
with a missing script URL and only attach an error handler to it.
Once the test callback returns, nothing keeps the worker rooted from
JavaScript. If GC ran before the WebWorker process reported the
script fetch failure, the Worker/WorkerAgentParent cycle could be
collected and the error event never delivered, leaving the test hung
until timeout.
Hold startup-pending WorkerAgentParents from the outside
EnvironmentSettingsObject and release that edge once the script load
succeeds, fails, or the worker closes. The worker now survives long
enough to deliver its first script-load result.
The HTML parser's script end tag algorithms save the current insertion
point in an "old insertion point" local before executing a script, then
restore that local after script execution. Ladybird modeled that local
as a single tokenizer field, so nested script execution via
document.write() could overwrite the outer script's saved value.
Keep a stack of old insertion points instead, and adjust saved offsets
when document.write() inserts new input before them. This keeps the
normal script and SVG script paths aligned with the spec text while
leaving the parser-blocking script resume path to set the insertion
point to undefined again.
When a click handler calls history.replaceState and the link's
cross-document activation behavior runs in the same task, the queued
sync step runs apply-the-history-step on the navigable mid-navigation,
transitioning its ongoing navigation through "traversal" and back to
null. That aborts the link's navigate event and bails out its deferred
work, leaving the link nav abandoned.
Skip this transient when the navigable already has a fresh ongoing
navigation. No major engine reproduces the race; long term, sync
same-document nav should bypass the traversal queue entirely (matching
Chromium).
This solves the long-standing issue where clicking on a box of tea
on https://twinings.co.uk/ would freeze the browser. :^)
When content-visibility:hidden starts skipping a subtree, clear the
stale layout and paint nodes for its descendants.
Preserve SVG mask, clipPath, and pattern resource boxes only when
they are attached to a referencing layout subtree outside the subtree
being hidden. Resources used inside the hidden subtree are cleared
with that subtree, so they rebuild under the live referencing layout
node when the subtree becomes visible again.
Otherwise observed descendants can keep old paintables around after
the current paint tree has been rebuilt without them.
Top-level navigation requests use the document fetch destination. CSP's
effective directive algorithm does not list document as a handled fetch
request destination, but our fallback path treated it like an unknown
fetch destination and applied connect-src.
Return no effective fetch directive for document destinations. This lets
top-level navigation use the CSP navigation checks instead. Keep nested
navigation on the existing frame and iframe path, since HTML rewrites
such requests to the container local name when a navigable has a
container.
This makes https://reddit.com/ load instead of redirecting to a blocked
challenge reponse URL.
Skip the generic HasProperty and Get loop when indexOf operates on a
simple packed array. In that case every index below length is an own
data property, so a direct scan of the packed indexed property storage
gives the same strict-equality result without the per-element property
lookup
ceremony.
Only use the fast path when the current packed storage size still
matches the length captured before fromIndex coercion, since that
coercion can run user code and mutate the receiver. Add coverage for
length and storage mutations during fromIndex coercion.
The parser only set `might_need_arguments_object` when an `arguments`
or `eval` Identifier went through `consume()`, but shorthand object
properties create the reference via `make_identifier()` directly. As
a result `function f() { return { arguments } }` allocated an
`arguments` local, never initialized it, and crashed at runtime when
the property was read.
Fall back to scope-driven detection: if scope analysis allocated a
non-lexical `arguments` local for the function, treat it as a real
arguments-object reference and emit `CreateArguments`. Skip the
fallback when a function declaration named `arguments` claims the
local, since that local belongs to the function, not the arguments
object.
Add a runtime test covering shorthand inside a free function and a
method, plus a regression test for `({ eval } = ...)` to confirm
destructuring assignment doesn't accidentally trigger arguments
materialization.
The scope collector stored identifier_groups and variables in
HashMaps and then sorted them alphabetically before assigning local
register indices. The sorts existed only because HashMap iteration
order is non-deterministic; alphabetical was a stable choice for
comparing bytecode against the now-removed C++ port.
Switch both maps to indexmap::IndexMap so iteration follows the order
of first reference (= source order), and drop the alphabetical sorts.
Local indices now reflect declaration order, which matches what shows
up in bytecode dumps and is easier to read alongside the source.
Add a focused bytecode test using zebra/yak/aardvark to pin the new
allocation order; existing tests using let/var declarations have
their local indices renumbered to match.
ECMAScript hoisting keeps the LAST function declaration with a given
name. The Rust scope_collector and script GDI extraction implemented
this with a single reverse scan that pushed first-seen entries, which
left the resulting list in REVERSE source order. The C++ side then
iterated `m_functions_to_initialize.in_reverse()` to undo that.
Switch the Rust side to a two-pass forward scan that records the last
position per name and emits entries in source order, and drop the
matching `.in_reverse()` calls in Script.cpp and AbstractOperations.cpp.
Same hoisting semantics; NewFunction emission and global property
iteration order now follow the source.
The HashMap that tracks last positions is keyed on `SharedUtf16String`,
so each insert is a refcount bump on the AST's existing Rc instead of
a deep `Vec<u16>` clone.
Add bytecode tests at script and nested-function scope that exercise
multiple declarations and a duplicate name to pin the new ordering.
The Rust parser used to copy several "rule_start"-derived positions
from the C++ implementation: every identifier inside a binding pattern
inherited the pattern's `[`/`{` position, every property identifier
after `.` inherited the period's position, every spread element
inherited the surrounding `[`/`{` position, and identifier-name
property keys inherited the object/class start position. This was
useful while comparing bytecode against the C++ port; with the C++
side gone, those quirks just hide the actual source positions in
source maps and devtools.
Drop the dedicated `binding_pattern_start` parser field and the
`ident_pos_override` parameter on `parse_property_key`, and capture
each identifier's own start position at the consume site.
Add an AST snapshot test that pins the new per-identifier positions
for object, array, nested, and parameter binding patterns.
The parser used to suppress the arguments/eval reference check via a
state flag that was set during the entire `parse_property_key` call.
That was over-broad: identifiers inside a computed property key like
`{ [arguments]: 1 }` are real references, but the flag silenced their
check too, leaving the function unmarked as needing the arguments
object. Reading the resulting property at runtime crashed.
Replace the flag with a `consume_property_key_token()` method used at
the specific consume sites for the property key token itself, so the
suppression is narrow. Inner consumes inside computed keys now go
through regular `consume()` and run the check normally.
Add a focused AST snapshot test covering plain, shorthand, computed,
binding-pattern, and method-name property-key cases.
When the HTML parser blocks on a synchronous external script, run a
separate tokenizer over the unparsed input and issue speculative fetches
for the resources it finds (script src, link rel=stylesheet|preload, img
src), with <base href> tracking and template/foreign-content skipping.
Also fills in the previously-stubbed "consume a preloaded resource"
algorithm and the document's "map of preloaded resources", so that
<link rel="preload"> followed by a matching consumer deduplicates to
a single fetch.
Generate exact JS buffer types for exact IDL buffer arguments instead
of widening them to BufferSource or ArrayBufferView.
This fixes cases like TextEncoder.encodeInto(), whose IDL requires
a Uint8Array destination. Previously the generated binding accepted
any BufferSource, so DataView, other typed arrays, and
ArrayBuffer-backed values were let through. With exact conversion,
those are rejected at the binding layer as expected.
The img inside a <picture> has to re-run "update the image data" when
nearby <source> elements change, so script-driven swaps of srcset (and
the other dimension/media attributes) actually take effect.
Per the HTML spec, the relevant mutations for an img element include:
"The element's parent is a picture element and a source element that
is a previous sibling has its srcset, sizes, media, type, width or
height attributes set, changed, or removed."
The same applies to source insertion, moving, and removal.
Fixes image loading on https://www.apple.com/mac/
We now track when a parent has a child affected by a backward structural
pseudo-class. These are selectors whose match result for an element can
depend on siblings after that element, such as `:last-child`,
`:only-child`, `:last-of-type`, `:only-of-type`, `:nth-last-child`, and
`:nth-last-of-type`.
When inserting or removing a node, previous siblings only need style
invalidation if one of them was matched against such a selector. Use the
parent-level flag to skip the previous-sibling walk when no child under
that parent can be affected.
This saves a lot of invalidation work on sites that insert a lot of
nodes into the DOM via JS.
Spinning a nested event loop to wait for a parser-blocking script blocks
the calling thread, can deadlock, and creates reentrancy hazards. Switch
to an event-driven pause/resume model, mirroring the prior
HTMLParserEndState refactor (df96b69e7a).
Three WPT document.write tests flip from Fail to Pass and are
rebaselined: all write an external script via document.write() followed
by inline content. With spin_until, control did not return to the caller
of document.write() between writing the script and observing its effects
so the test's order assertions saw a different sequence than the spec
mandates.
Several structural-* stress tests iterated over enough scopes,
mutation cases, and modes to time out on the slower CI runners.
Split each timing-out test along mode boundaries so no single file
runs more than a quarter of the original work, mirroring the
warm/cold/pseudo/detached split shape across the suite:
* structural-descendant-stress: split scope-wise into doc/shadow,
slot, and part files (already there from the earlier commit),
then mode-wise into attached/detached and warm/pseudo halves.
* structural-descendant-state-stress: split mode-wise into the
same four-quadrant attached/detached x warm/pseudo layout.
* structural-descendant-direction-language-stress: ditto.
* structural-child-part-shadow-stress and structural-child-slot-
stress: same four-quadrant split (these already had no -cold
detached modes so the detached half is smaller).
Every resulting file is well under 700 lines of expected output,
keeping the per-test runtime comfortably below the CI timeout.