Expand description
§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 smix_screen::{A11yNode, Rect, Role};
use smix_selector::{
AnchorBox, IndexModifiers, Modifiers, Pattern, Selector,
describe_selector, match_text, match_text_compiled,
};
// Plain text base
let s1: Selector = Selector::Text {
text: Pattern::text("Login"),
modifiers: Modifiers::default(),
};
// Spatial modifier — "Submit" below "Email"
let s2: Selector = Selector::Text {
text: Pattern::text("Submit"),
modifiers: Modifiers {
below: Some(Box::new(Selector::Text {
text: Pattern::text("Email"),
modifiers: Modifiers::default(),
})),
..Default::default()
},
};
// Regex (auto case-insensitive via /(?i)/ prefix — maestro parity)
let s3 = Selector::Text {
text: Pattern::regex("^log"), // matches "Login", "Log out", etc.
modifiers: Modifiers::default(),
};
// Anchor-only — pick the n-th element below a reference
let s4 = Selector::Anchor {
anchor: AnchorBox {
below: Some(Box::new(Selector::Text {
text: Pattern::text("Header"),
modifiers: Modifiers::default(),
})),
..Default::default()
},
index: IndexModifiers { nth: Some(2), ..Default::default() },
};
// AI-readable rendering for failure prompts and logs
let s = describe_selector(&s2);
assert!(s.contains("below="));
// Hot-path matching: compile once, match many times
let pattern = Pattern::text("Login");
let compiled = pattern.compile().expect("text compile is infallible");
let node = A11yNode { /* ... */
};
assert!(match_text_compiled(&node, &compiled));§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.
smix-selector — Selector enum + Modifiers + AnchorBox + match_text
(stone). Ported from now-retired TS source: src/core/selector.ts +
src/core/resolve-selector.ts:153-184.
§Scope
- Pure types (
Selector,Pattern,Modifiers,AnchorBox,IndexModifiers) with serde wire compatibility (camelCase JSON, matching SDK / CLI / MCP / runner-client wire shape). - Pure functions (
match_text,describe_selector) — no I/O, no Tree traversal (resolver lives insmix-selector-resolver, c5+).
§Selector base forms (跟 TS 同源, v1.5 C2 lock-in)
6 mutually exclusive base forms:
- Text (string / regex)
- Id (string strict equal vs node.identifier)
- Label (string strict equal vs node.label)
- Role { role, name?: pattern } (role enum + optional pattern name)
- Focused (focused: true — runtime-resolved current focus target)
- Anchor (anchor-only, no own base; spatial keys do the filtering)
§Modifiers / AnchorBox (recursive)
Base 1-4 stack Modifiers (6 spatial + 3 index). Base 6 (Anchor)
stacks only IndexModifiers (no nested spatial — anchor IS the
spatial intent already). Base 5 (Focused) stacks nothing.
§Pattern wire form (string-or-regex union, serde-friendly)
TS string | RegExp doesn’t survive JSON serialization (RegExp →
{}). Wire form is:
"plain"(untagged string) — string strict equal (case-insensitive, v1.5 c5i-d maestro parity){ "regex": "pattern", "flags": "i" }(object tag) — regex form, auto-/i flag inject if absent
Runtime compile happens inside match_text (no cache at the stone
layer; resolver / SDK callers may memoize externally if needed).
Structs§
- Anchor
Box - Anchor base form’s spatial keys. Identical shape to the spatial half
of
Modifiersbut without nth/first/last (Anchor stacks IndexModifiers separately). - Index
Modifiers - IndexModifiers — the subset BaseAnchor can stack. Kept separate from
Modifiersso anchor callers can’t accidentally write the top-level spatial form (which would be two surfaces for the same intent). - Modifiers
- Modifier set stackable on Text / Id / Label / Role base forms. Spatial keys (near/below/above/leftOf/rightOf/inside) carry recursive selectors that resolve to an anchor node; index keys (nth/first/last) pick from the surviving candidate list.
- True
- Newtype around
true— used as theSelector::Focuseddiscriminator. Serializes/deserializes as the JSON literaltrue.
Enums§
- Compiled
Pattern - Compiled, hot-path-ready pattern. Caches the compiled
regex::Regex(跟 TS RegExp 对象内部 compile-once cache 同精神 — TS RegExp lazily compiles on first.test()and re-uses the compiled DFA; we eager- compile viaPattern::compileand store the result for the same effect on Rust side). - Pattern
- String-or-regex pattern. Wire-compatible: plain JSON string ↔
Pattern::Text; tagged object{regex, flags}↔Pattern::Regex. - Role
- Accessibility role enum (29 variants).
- Selector
- Selector — 6 mutually exclusive base forms.
Functions§
- describe_
selector - Human / AI-readable rendering of a
Selectorfor error prompts and logs. Stable enough that AI can pattern-match against past failures. Mirrors TSdescribeSelectorinsrc/core/selector.ts:118-1601:1. - match_
text - Match a
Patternagainst the six text-bearing fields of a node (label,title,value,placeholderValue,identifier,text) — OR semantics, case-insensitive. SDK convenience wrapper that compiles the pattern on every call; for hot loops preferPattern::compileonce +match_text_compiledper node. - match_
text_ compiled - Text matching against an
A11yNode. 1:1 with TSsrc/core/resolve-selector.ts:153-184(matchText).