smix-selector
Structured Selector enum + match_text for iOS-Simulator a11y trees.
Six mutually-exclusive base forms (text / id / label / role+name /
focused / anchor-only), recursive spatial modifiers
(near/below/above/leftOf/rightOf/inside), and a Pattern / CompiledPattern
split that gives JS-RegExp-like ergonomics with zero per-call compile
cost on the resolver hot path.
Quickstart
use ;
use ;
// Plain text base
let s1: Selector = Text ;
// Spatial modifier — "Submit" below "Email"
let s2: Selector = Text ;
// Regex (auto case-insensitive via /(?i)/ prefix — maestro parity)
let s3 = Text ;
// Anchor-only — pick the n-th element below a reference
let s4 = Anchor ;
// AI-readable rendering for failure prompts and logs
let s = describe_selector;
assert!;
// Hot-path matching: compile once, match many times
let pattern = text;
let compiled = pattern.compile.expect;
let node = A11yNode ;
assert!;
Pattern / CompiledPattern split
The wire form of a text or regex selector pattern is Pattern
(serde-friendly: a plain string for text, {regex, flags} object for
regex). Matching uses CompiledPattern (caches the compiled
regex::Regex DFA). Callers transition via Pattern::compile().
This split mirrors how V8's RegExp lazily compiles + reuses the DFA
once per object. In Rust we make the compile step explicit so the
resolver layer can amortize compile cost across hundreds of candidate
nodes in a tree walk:
| Operation | TS V8 baseline | Rust hot path | Speedup |
|---|---|---|---|
match_text string field-1 hit |
36.8 ns | 4.89 ns (match_text_compiled) |
7.5× |
match_text string 6-field scan miss |
41.4 ns | 3.53 ns | 12× |
match_text regex hit (default /i) |
105.5 ns | 9.39 ns | 11× |
match_text regex on 100-char label |
52.2 ns | 31.5 ns | 1.7× |
Pattern::compile regex (one-time, amortized) |
n/a | 15.65 µs | — |
For one-shot SDK calls there is also match_text(&node, &Pattern) which
compiles on every call (15 µs); resolver-driven hot loops should always
prefer match_text_compiled.
SDK convenience vs resolver hot path
smix-selector exposes two matcher entry points, and the choice between
them matters by ~10000× for regex patterns:
| API | When | Cost (regex) | Cost (plain text) |
|---|---|---|---|
match_text(node, &Pattern) |
One-shot ad-hoc calls; ergonomic for tests, REPLs, and tooling that matches a handful of nodes | ~15 µs / call (regex::Regex::new every time) |
~5 ns / call |
match_text_compiled(node, &CompiledPattern) |
Hot loops; the real path inside smix-selector-resolver (HashMap<*const Pattern, CompiledPattern> cache + DFS) and smix-driver |
~5–50 ns / call (compile cost paid once) | ~5 ns / call |
If you write your own resolver, walker, or any code that calls a matcher
in a loop, reach for Pattern::compile() once at the call site and feed
the resulting CompiledPattern to match_text_compiled per node — the
pattern documented in smix-selector-resolver
ResolverContext. A regression guard
(crates/smix-selector/tests/perf_contract.rs) blocks bare hot-loop
smix_selector::match_text calls from landing in workspace src/.
When to reach for this
| Use case | Pick |
|---|---|
| Need a structured query type for a11y trees / accessibility selectors | smix-selector |
| Want maestro-equivalent text matching (6-field OR + case-insensitive) | smix-selector |
| Want recursive spatial modifiers (e.g. "Submit below Email") | smix-selector |
| Need xpath / CSS selectors | use a generic query crate; this is iOS-shaped |
| Just need string matching, no tree | use regex directly |
Scope
- ✅ 6 base forms × recursive spatial modifiers × index modifiers
- ✅ camelCase JSON wire (no discriminator field; presence of
text/id/label/role/focused/anchoris the discriminator) - ✅
Pattern::compile()→CompiledPattern(regex cache) - ✅
match_text6-field OR scan matching maestro parity - ✅
describe_selectorAI-readable rendering - ❌ No tree walking (use
smix-selector-resolver) - ❌ No xpath / CSS
License
Dual-licensed under either:
at your option.