Expand description
§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 smix_screen::{A11yNode, Rect};
use smix_selector::{Modifiers, Pattern, Selector};
use smix_selector_resolver::{resolve_selector, resolve_selector_all};
let tree = A11yNode {
raw_type: "application".into(), element_type_raw: 1,
bounds: Rect { x: 0.0, y: 0.0, w: 390.0, h: 844.0 },
children: vec![
A11yNode { /* "Login" button */
bounds: Rect { x: 50.0, y: 100.0, w: 200.0, h: 40.0 },
enabled: true, selected: false, has_focus: false, visible: true,
children: vec![],
},
],
};
let selector = Selector::Text {
text: Pattern::text("Login"),
modifiers: Modifiers::default(),
};
let one = resolve_selector(&tree, &selector);
assert!(one.is_some());
let all = resolve_selector_all(&tree, &selector);
assert_eq!(all.len(), 1);§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.
smix-selector-resolver — Selector resolution against an A11yNode
tree (stone, hot path).
Ported from now-retired TS source: src/core/resolve-selector.ts (279 lines, v1.5
c5i-d + v1.6 c4 lock-in). 1:1 semantics:
- Collect: DFS pre-order over the tree, collect every node whose base form matches (text / id / label / role+name / focused / anchor accept-all).
- Visibility filter (v1.5 c5i-d): drop nodes whose bounds are zero or completely outside the tree viewport — unless no candidate is visible, then drop nothing (TS semantic preserves miss reports).
- Spatial filter (v0.3 C5): for each
near/below/above/leftOf/rightOf/insidekey (top-level for base 1-4, or insideanchor.*for base 6), recursively resolve the anchor sub-selector; filter candidates by centroid-axis or geometric containment. AND semantics. Anchor null → overall null. - Index pick (v0.3 C5): apply
first/last/nthin declaration order; later overrides earlier (TS line 273-278 —nthwins if bothfirstandnthset).
§Pattern compile cache
Selector trees carry Pattern (wire form). Every text/regex match
site re-compiles the regex unless cached. The resolver builds a
per-call ResolverContext that walks the selector tree once, calls
Pattern::compile on every Pattern node, and stores the
resulting CompiledPattern keyed by the raw pointer to the wire
Pattern (stable for the lifetime of the borrowed selector).
This is the key perf gain over the TS resolver (which lacks an
explicit cache — V8 RegExp object internally lazy-compiles, but every
matchText call still pays the per-candidate fold). For a 100-node
tree with a single text base, Rust 1× compile + 100 × match_compiled
≪ TS 100 × compile-and-match.
Structs§
- Resolver
Context - Per-call cache mapping raw pointer of each
Patternin the selector tree to itsCompiledPattern. Built once at entry; lookup is O(1) hash.
Functions§
- resolve_
selector - Resolve a
Selectoragainst anA11yNodetree. Returns the first surviving candidate after collect → visibility → spatial → index. - resolve_
selector_ all - Resolve all matching candidates (TS
resolveSelectorAll, line 89-96). - resolve_
selector_ all_ compiled - Resolve all matching candidates with a caller-provided
ResolverContext. Same asresolve_selector_allminus the per- call cache build. Seeresolve_selector_compiledfor the reuse pattern. - resolve_
selector_ compiled - Resolve a
Selectoragainst anA11yNodetree using a caller- providedResolverContext. Same pipeline asresolve_selectorbut skips the per-call cache build — pass a reusable context built once viaResolverContext::newto amortize regex compile cost across many calls (typical retry loop insmix-driver::wait_for/scroll). SeeResolverContextdoc for the lifetime contract.