Skip to content

WASM selector engine: cascade integration (Milestone 3) - #2167

Draft
lukasbob wants to merge 2 commits into
mainfrom
feat/alfa-selector-wasm
Draft

lukasbob wants to merge 2 commits into
mainfrom
feat/alfa-selector-wasm

Conversation

@lukasbob

Copy link
Copy Markdown

Summary

Continues the alfa-selector-wasm effort (a Rust/WASM selector engine built on Servo's selectors crate, originally motivated by a nesting bug where desugared :is()-wrapped selectors from & rules didn't match correctly).

  • Milestone 2 (first commit): the standalone engine — parses and matches selectors against a real Alfa DOM via a binary serialization bridge.
  • Milestone 3 (second commit): wires the engine into alfa-cascade's SelectorMap. Candidate matching is delegated to WASM wherever it can parse the selector, falling back to the existing TypeScript matcher otherwise (currently: selectors using a Context-dependent pseudo-class like :hover/:focus-within, since those need Context-state support the engine doesn't have yet). :host/::slotted selectors are untouched.

Also includes prerequisite fixes this surfaced:

  • :has() matching was silently broken in the Rust engine (ElementRef::opaque() derived its identity token from a transient stack address rather than the arena index, so the selectors crate's relative-selector anchor check always failed).
  • alfa-selector-wasm is now wired into the standard TS project-reference build so alfa-cascade can depend on it normally.
  • SelectorEngine is now a process-wide singleton with idempotent loadDom(), since the compiled WASM module's DOM arena is process-global but Cascade keeps many documents' cascades alive concurrently.
  • alfa-selector's Selector#toString() leaked a literal "of None" into serialized bare :nth-child()/:nth-last-child() selectors, which would've forced every such selector to fall back to TS matching unnecessarily.

Known gap carried forward: the original motivating nesting bug isn't fixed end-to-end yet. alfa-dom's real-world CSSOM ingestion (native.ts) doesn't capture nested style rules at all, so a raw & selector never reaches the cascade today — only already-desugared selectors do. That's separate alfa-dom work, tracked outside this PR.

Test plan

  • Rust unit tests for :has() matching (direct child, deep descendant, sibling combinator, absence, composition with :is())
  • alfa-selector-wasm's Alfa-integrated vitest suite (bridge.spec.tsx)
  • alfa-cascade test suite, including a new end-to-end test matching a WASM-backed :is() + :has() selector through the full Cascade
  • Full downstream regression pass: alfa-selector, alfa-selector-wasm, alfa-cascade, alfa-style, alfa-rules — 2041 tests passing, no type errors
  • Two changesets added (alfa-selector patch, alfa-cascade patch)

🤖 Generated with Claude Code

lukasbob and others added 2 commits August 31, 2026 09:56
WASM bindings around Servo's `selectors` crate, intended to eventually
replace Alfa's TypeScript selector parsing and matching in
alfa-cascade. Motivated by a nesting bug where desugared `:is()`
selectors from `&` rules don't match correctly in the current engine.

Milestone 2: ingests a real Alfa DOM via a binary serialization
bridge, parses selectors (including :is(), :where(), :has(), &
nesting), and matches type/class/id/attribute/descendant/child/
sibling/:root/:nth-child() selectors against real Alfa elements, plus
specificity. Cascade integration (Milestone 3) and & nesting
resolution (Milestone 4) are not yet started.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires the WASM selector engine into alfa-cascade's SelectorMap: candidate
matching is delegated to it wherever it can parse the selector, falling
back to the existing TypeScript Selector#matches otherwise (currently
anything using a Context-dependent pseudo-class, e.g. :hover/:focus-within,
since those need Context-state support the engine doesn't have yet).
:host/::slotted selectors are unaffected, they already matched through
their own matchHost/matchSlotted paths.

Also includes the prerequisite work this surfaced:
- Fixed :has() matching in the Rust engine: ElementRef::opaque() derived
  its identity token from a transient stack address rather than the arena
  index, so the selectors crate's relative-selector anchor check always
  failed. selector-wasm is now wired into the standard TS project-reference
  build (packages/tsconfig.json, ts/tsconfig.json, test/tsconfig.json) so
  alfa-cascade can depend on it normally.
- Made SelectorEngine a process-wide singleton with idempotent loadDom():
  the compiled WASM module's DOM arena is process-global, but Cascade keeps
  many documents' cascades alive concurrently, so matching now always
  (re)asserts the right tree is loaded first.
- Fixed alfa-selector's Selector#toString() leaking a literal "of None"
  into serialized bare :nth-child()/:nth-last-child() selectors, which
  would otherwise force every such selector to fall back to TS matching
  unnecessarily.

Note: the original motivating nesting bug isn't fixed end-to-end yet.
alfa-dom's real-world CSSOM ingestion (native.ts) doesn't capture nested
style rules, so a raw `&` selector never reaches the cascade today; only
already-desugared selectors do. That's separate alfa-dom work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1975a02

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 77 packages
Name Type
@siteimprove/alfa-selector Patch
@siteimprove/alfa-cascade Patch
@siteimprove/alfa-act Patch
@siteimprove/alfa-affine Patch
@siteimprove/alfa-applicative Patch
@siteimprove/alfa-aria Patch
@siteimprove/alfa-array Patch
@siteimprove/alfa-bits Patch
@siteimprove/alfa-branched Patch
@siteimprove/alfa-cache Patch
@siteimprove/alfa-callback Patch
@siteimprove/alfa-clone Patch
@siteimprove/alfa-collection Patch
@siteimprove/alfa-comparable Patch
@siteimprove/alfa-continuation Patch
@siteimprove/alfa-css-feature Patch
@siteimprove/alfa-css Patch
@siteimprove/alfa-device Patch
@siteimprove/alfa-dom Patch
@siteimprove/alfa-eaa Patch
@siteimprove/alfa-earl Patch
@siteimprove/alfa-either Patch
@siteimprove/alfa-emitter Patch
@siteimprove/alfa-encoding Patch
@siteimprove/alfa-equatable Patch
@siteimprove/alfa-flags Patch
@siteimprove/alfa-fnv Patch
@siteimprove/alfa-foldable Patch
@siteimprove/alfa-functor Patch
@siteimprove/alfa-generator Patch
@siteimprove/alfa-graph Patch
@siteimprove/alfa-hash Patch
@siteimprove/alfa-http Patch
@siteimprove/alfa-iana Patch
@siteimprove/alfa-iterable Patch
@siteimprove/alfa-json-ld Patch
@siteimprove/alfa-json Patch
@siteimprove/alfa-lazy Patch
@siteimprove/alfa-list Patch
@siteimprove/alfa-map Patch
@siteimprove/alfa-mapper Patch
@siteimprove/alfa-math Patch
@siteimprove/alfa-monad Patch
@siteimprove/alfa-network Patch
@siteimprove/alfa-option Patch
@siteimprove/alfa-painting-order Patch
@siteimprove/alfa-parser Patch
@siteimprove/alfa-performance Patch
@siteimprove/alfa-predicate Patch
@siteimprove/alfa-record Patch
@siteimprove/alfa-rectangle Patch
@siteimprove/alfa-reducer Patch
@siteimprove/alfa-refinement Patch
@siteimprove/alfa-result Patch
@siteimprove/alfa-rng Patch
@siteimprove/alfa-rules Patch
@siteimprove/alfa-sarif Patch
@siteimprove/alfa-selective Patch
@siteimprove/alfa-selector-wasm Patch
@siteimprove/alfa-sequence Patch
@siteimprove/alfa-set Patch
@siteimprove/alfa-slice Patch
@siteimprove/alfa-string Patch
@siteimprove/alfa-style Patch
@siteimprove/alfa-table Patch
@siteimprove/alfa-test Patch
@siteimprove/alfa-thunk Patch
@siteimprove/alfa-time Patch
@siteimprove/alfa-toolchain Patch
@siteimprove/alfa-trampoline Patch
@siteimprove/alfa-tree Patch
@siteimprove/alfa-trilean Patch
@siteimprove/alfa-tuple Patch
@siteimprove/alfa-url Patch
@siteimprove/alfa-wcag Patch
@siteimprove/alfa-web Patch
@siteimprove/alfa-xpath Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

});

test("matches descendant and child combinators across the real tree", (t) => {
const { document, div, table, span } = fixture();
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.

2 participants