IsMatcher handled multi-node selections by calling m.Filter(s.Nodes) and
checking whether the returned slice was non-empty, forcing the matcher to walk
the whole selection and materialize all matches even though the API only needs
an existence check.
This commit replaces this with a simple loop over s.Nodes that calls m.Match(n)
and returns true on the first match. This also makes the single-node special
case unnecessary.
Benchmark results on arm64 over 10 runs:
name old time/op new time/op delta
Is-8 4.67µs 1.32µs -71.7%
IsPositional-8 21.44µs 0.82µs -96.2%
name old B/op new B/op delta
Is-8 96 80 -16.7%
IsPositional-8 1144 184 -83.9%
name old allocs new allocs delta
Is-8 5 4 -20.0%
IsPositional-8 11 7 -36.4%
These methods were building a full filtered Selection (via
FilterFunction/FilterSelection/FilterNodes -> winnow -> grep) only to
check Length() > 0. This commit replaces with early-return loops that stop at
the first match, significantly speeding them up:
name old ns/op new ns/op delta
IsFunction-8 1530 3.9 -99.7%
IsSelection-8 1890 19.9 -98.9%
IsNodes-8 2016 19.9 -99.0%
name old B/op new B/op delta
IsFunction-8 784 0 -100.0%
IsSelection-8 72 0 -100.0%
IsNodes-8 72 0 -100.0%
name old allocs new allocs delta
IsFunction-8 28 0 -100.0%
IsSelection-8 3 0 -100.0%
IsNodes-8 3 0 -100.0%