smix-selector-resolver
End-to-end resolver for smix-selector
queries against an smix-screen::A11yNode
tree. Pipeline: DFS pre-order collect → visibility filter → spatial
filter (six recursive anchor keys, AND semantics) → index pick
(first / last / nth). One per-call compile cache (ResolverContext)
amortizes regex compilation across the entire candidate walk.
Quickstart
use ;
use ;
use ;
let tree = A11yNode ;
let selector = Text ;
let one = resolve_selector;
assert!;
let all = resolve_selector_all;
assert_eq!;
Performance
End-to-end resolve_selector measurements on M-series macOS:
| Operation | TS V8 baseline | Rust release | Speedup |
|---|---|---|---|
| text base, 100-node tree, label hit (DFS depth 2) | 3,652 ns | 1,186 ns | 3.1× |
| text base, 100-node tree, full DFS miss | 3,693 ns | 1,088 ns | 3.4× |
| text base + spatial below modifier, 4-node tree | 607 ns | 295 ns | 2.1× |
| id base, 100-node tree, miss | 3,234 ns | 198 ns | 16× |
| regex text base, 100-node tree, label hit | 11,114 ns | 10,957 ns | parity (regex compile-dominant) |
| anchor-only + below, 4-node tree, nth=1 | 532 ns | 277 ns | 1.9× |
| text + nth=2, multi-match 5-X tree | 468 ns | 179 ns | 2.6× |
Numbers reproducible via
cargo bench --bench resolver -p smix-selector-resolver.
The regex case sits at parity (≈11 µs) because regex compile dominates;
consumer-level caches (e.g. caching the compiled selector across
multiple wait_for poll iterations) push that case to ~11× as well.
Pipeline
- Collect: DFS pre-order over the tree, gather every node where the base form matches (text / id / label / role+name / focused / anchor accept-all).
- Visibility filter (from
smix-screen): drop nodes with zero/negative bounds or fully-outside-viewport — unless no candidate is visible, in which case drop nothing (preserves miss reporting paths). - Spatial filter: for each of
near/below/above/leftOf/rightOf/inside(top-level or insideanchor.*), recursively resolve the anchor sub-selector; filter candidates by centroid-axis or geometric containment. AND semantics. Anchor null → overallNone. - Index pick: apply
first/last/nth; later modifier overrides earlier (nthwins both).
When to reach for this
| Use case | Pick |
|---|---|
Have an A11yNode tree + structured Selector → need the matched node |
smix-selector-resolver |
| Just need string match against a single node | smix-selector::match_text directly |
| Need xpath / CSS / jQuery-style selectors | use a generic query crate |
Scope
- ✅ Pure function, no async, no I/O
- ✅
ResolverContextregex compile cache (1 compile perresolve_selectorcall, amortized over all candidates) - ✅ Anchor null short-circuits to
None - ✅ 30 unit tests + 2 perf-gate budgets enforced in CI
- ❌ No tree fetching (use
smix-runner-client) - ❌ No tap/coord injection (use
smix-host-coord-resolver+ an injector)
License
Dual-licensed under either:
at your option.