Skip to content

Commit 573a0b5

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
v3: enum-tagged dispatch for singleton search strategies (DRAFT — ignore until reviewed by @ChrisRackauckas) (#73)
* v3: enum-tagged dispatch for singleton search strategies Replaces the v2 `Base.searchsortedlast(::SearchStrategy, ...)` multimethod dispatch on strategy struct singletons with a single FFF-owned dispatcher tagged by a `StrategyKind` enum. The runtime `if/elseif` over the enum value is well-predicted in hot loops, the kernel bodies inline, and the return path stays concrete (`Int`) regardless of which kind is picked at runtime — none of the `Union`-return pathology that v2's design suffered from when the chosen strategy depended on runtime data (e.g. `Auto`'s decision tree returning `Union{BracketGallop, LinearScan, BinaryBracket}`). The `bench/enum_vs_typeparam_dispatch.jl` sweep confirms ~0 ns overhead vs. the v2 path across 20 representative cells (worst case +2.2%, several cells show enum dispatch beating legacy multimethod by 3-6%). Stateful strategies (`Auto`, `GuesserHint`) stay on the multimethod path because they carry per-instance data that doesn't fit into a singleton tag. Layout: - src/kinds.jl: `@enum StrategyKind` + `search_last` / `search_first` enum dispatchers. - src/kernels.jl: per-strategy kernel functions (`_kernel_last_bracket_gallop`, etc.), lifted out of the v2 method bodies. No semantic changes. - src/legacy_dispatch.jl: `Base.searchsortedlast(::S, ...)` shims for each singleton strategy struct, forwarding to `search_last(KIND_X, ...)`. Scheduled for removal in v4. - src/strategies.jl: `Auto` now holds a `StrategyKind` field. `Auto()` defaults to `KIND_BINARY_BRACKET`; `Auto(v)` resolves the kind from `length(v)` + `SearchProperties(v)`. - src/auto.jl: `Auto`'s per-query `search_last` / `search_first` is a one-line forward to the stored kind. The batched dispatcher re-resolves the kind from `(v, queries)` (gap heuristic). - src/guesser.jl: `GuesserHint` dispatches via `search_last(::GuesserHint, ...)` method (no kind tag; stateful). - src/findequal.jl: `findequal(::StrategyKind, v, x[, hint])` direct. Back-compat: every v2 call site (e.g. `searchsortedlast(BracketGallop(), v, x, hint)`) still works via the shim — confirmed by the existing 100,472-test suite (now 104,796 tests with the new v3 API safetestset). DataInterpolations.jl: needs zero updates to work with v3. Optional optimisation: replace `A.strategy = FindFirstFunctions.BracketGallop()` with `A.strategy = FindFirstFunctions.KIND_BRACKET_GALLOP` and `searchsortedlast(strat, ...)` with `FindFirstFunctions.search_last(strat, ...)` to skip the shim layer. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * v3: parametric SearchProperties{T} + props-aware UniformStep Make `SearchProperties` parametric on the data ratio type `T = typeof(oneunit(eltype(v)) / oneunit(eltype(v)))` and add two new fields: - `first_val::T` — `v[1]` (or `first(r)` for an AbstractRange) - `inv_step::T` — precomputed `1 / step`, or `(n-1)/(v[end]-v[1])` for a uniform AbstractVector These fields are populated when `is_uniform = true` and zero otherwise. They feed a new props-aware `UniformStep` kernel invoked by `Auto(v)` when the resolved kind is `KIND_UNIFORM_STEP`: closed-form O(1) lookup with one subtract, one multiply, one truncate (no per-query float division). This folds in the never-merged `DirectStep` strategy. `Auto{T}` is now parametric, carrying `SearchProperties{T}`. `Auto(v)` returns `Auto{T}` where `T` is the ratio type of `eltype(v)`. Two `Auto`s constructed from data with the same ratio type share one concrete type (e.g. `Vector{Int}` and `Vector{Float64}` both promote to `Auto{Float64}`), so `Vector{Auto{Float64}}` is concrete. The raw `UniformStep()` singleton keeps its old behaviour for back-compat: `searchsortedlast(UniformStep(), r, x)` still does `fld(diff, step)` per query. Only `Auto(v)` routes through the props-aware path. Bench (per-query latency, n = 10k, m = 1000): Sorted queries on AbstractRange (0.0:0.5:N): Auto(r) [props]: 13.74 ns/q UniformStep() [fld]: 65.02 ns/q BracketGallop+hint: 44.29 ns/q Sorted queries on uniform Vector{Float64}: Auto(v) [props]: 7.6 ns/q UniformStep() [bb]: 81.79 ns/q (vector path falls back to BinaryBracket) BracketGallop+hint: 25.43 ns/q Random queries on AbstractRange: Auto(r) [props]: 13.73 ns/q UniformStep() [fld]: 67.5 ns/q BracketGallop+hint: 238.41 ns/q (miss path) The props-aware UniformStep beats raw UniformStep by 5-10× and is ~3-20× faster than BracketGallop on uniformly-spaced data. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * SearchProperties: avoid props-aware path for non-Real numeric eltypes `SearchProperties(::AbstractVector{<:Number})` used to call `T(v[1])` unconditionally, which trips a `DimensionError` on Unitful `Quantity` (or any other non-`Real` numeric type whose `T(::Quantity)` conversion is ill-defined). Split the overload: - `AbstractVector{<:Real}` keeps the props-aware path: populate `first_val::T` / `inv_step::T` so `Auto(v)` routes through the closed-form `KIND_UNIFORM_STEP` kernel. - `AbstractVector{<:Number}` (non-`Real`) runs only the linearity probe (which works on any `Number`), returns `SearchProperties{Float64}` with `is_uniform = false`, and leaves `first_val` / `inv_step` zero. Auto then resolves to `KIND_BRACKET_GALLOP` (or `KIND_LINEAR_SCAN`), matching v2 behaviour. `_auto_is_uniform` is correspondingly narrowed: `AbstractRange{<:Real}` remains "always uniform", but for `AbstractRange{<:Number}` (e.g. `StepRange{Quantity}`) we consult `props.is_uniform`, which is `false` under the new overload. This prevents the closed-form kernel from being invoked on Unitful ranges. Fixes the `DataInterpolations.jl` test failure under Unitful eltypes: `LinearInterpolation(u::Vector{Quantity{...,𝐋}}, t::StepRange{Quantity{...,𝐓}})`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * test: relax Auto-on-AbstractRange allocation bound for Julia 1.10 `SearchProperties{Float64}` now carries `first_val::Float64` and `inv_step::Float64`, growing `Auto{Float64}` from 16 to 32 bytes. Julia 1.10's kwarg trampoline boxes the resulting NamedTuple as exactly 64 bytes; 1.11 elides the allocation entirely. The `< 64` bound (which fit 1.11 with margin) now reads as `64 < 64` on 1.10. Bump to `<= 64` to cover both — the call is still a tight closed-form lookup, just with one more boxed kwarg pointer than v2 had. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Fix wrong-answer bugs in the props-aware UniformStep path Three correctness fixes for the closed-form O(1) lookup: - Clamp the float position f = diff * inv_step in the float domain before truncating. unsafe_trunc(Int, f) is UB once |f| exceeds typemax(Int), which any finite extreme query reaches; on x86 it returned typemin and the clamp then produced the wrong end of the vector (search_last(Auto(v), v, 1.0e300) returned firstindex). - Validate is_uniform exactly. The 11-point sampled probe can be fooled by data that is uniform at the probed points but jittered between them, and a false positive makes the closed form land in the wrong cell. A positive from the sampled pre-filter is now confirmed by an exact O(n) scan over every element before is_uniform is set. - Replace the kernels' single-step roundoff correction with a walk, and fall back to binary search when f is NaN (caller-forced is_uniform = true on a zero-span vector gives inv_step = Inf). A wrong closed-form guess now degrades to a slower search instead of a silently wrong index, which also keeps the searchsorted contract when a caller forces is_uniform on non-uniform data. Also guard the per-query Auto dispatch on props.has_props: an Auto holding the sentinel SearchProperties() has inv_step = 0, which made the closed-form guess degenerate into an O(n) walk from firstindex (~49,000x slower on a 1e6-element range). The sentinel case now routes to the fld-based kind kernel like the batched path already did. Regression tests for all four cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * findequal: route KIND_BISECT_THEN_SIMD to the SIMD shortcut The enum-tagged findequal forms went through the generic search_first + post-check path for every kind, so the documented v3 migration findequal(BisectThenSIMD(), v, x) -> findequal(KIND_BISECT_THEN_SIMD, v, x) silently lost the DenseVector{Int64} bisect-then-SIMD shortcut. Forward that kind to the struct form, which carries the specialized dispatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Clean up stale comments, docstrings, and dead code - kinds.jl / legacy_dispatch.jl claimed the back-compat shims emit a depwarn; they deliberately do not (NEWS documents why). State the actual v4-removal plan instead. - strategy_kind docstring said Auto throws; it returns the stored kind. - searchsortedrange docstring now describes the actual hint seeding (max(first_idx, hint) for the upper endpoint). - Drop the unused (and broken) bench_per_query function in the props bench. - Strip change-history narration from comments per repo convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Fix docs build: unresolvable @refs, ambiguous slug, missing @docs entry makedocs(strict on missing_docs/cross_references) failed on four counts carried over from the v2 docs: - [`searchsortedlast`](@ref Base.searchsortedlast) in batched.jl docstrings cannot resolve (Base docstrings are not in this build) — use plain text. - The [Equality search](@ref) link was ambiguous between the equality.md page title and a strategies.md heading of the same name — rename the heading and target the page slug explicitly. - _simd_scan_ir has no docstring, so its @ref fails — de-link it. - searchsortedrange has a docstring but appeared in no @docs block — add it to interface.md. Verified with a full local docs build (zero errors). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Drop one-off dev bench scripts; fix bench env compat for v3 enum_vs_typeparam_dispatch.jl and uniform_step_props_bench.jl were development sweeps used to justify the v3 dispatch design; their findings are recorded in NEWS.md and the PR discussion, and they have no ongoing maintenance value. The maintained sweeps referenced from the docs (auto_sweep.jl, analyze.jl, bitinterp_sweep.jl) stay. bench/Project.toml pinned FindFirstFunctions = "2.1.0", which cannot resolve against the dev'd 3.0.0 — bumped to "3". Verified the bench environment instantiates with the local v3 package and that every FindFirstFunctions symbol referenced by the remaining scripts exists in v3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Remove the v2 Base.searchsortedlast/searchsortedfirst extensions v3 is a breaking release — drop the back-compat layer instead of carrying it to v4. FindFirstFunctions no longer extends Base.searchsortedlast / Base.searchsortedfirst with strategy methods; search_last / search_first are the only search entry points. - Delete the per-singleton Base shims (legacy_dispatch.jl) and the Base shims for Auto and GuesserHint. - Add search_last / search_first methods that accept a singleton strategy struct directly, forwarding through strategy_kind (which constant-folds for literal strategies, so the struct form costs nothing over the kind form). The structs stay as the friendly strategy names; the file is now strategy_kind.jl. - findequal's struct form and searchsortedrange / the batched sorted loops now route through search_first / search_last internally. - Precompile workload, tests, NEWS, and the docs migration guide updated to the v3-only API. New guard testset asserts no Base.searchsorted strategy methods exist, against accidental reintroduction. The FFF-owned batched API (searchsortedlast! / searchsortedfirst! / searchsortedrange) and the equality API are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Docs: update remaining pages to the v3-only search API interface.md still described the v2 contract (extending Base.searchsortedlast/searchsortedfirst per strategy); it now documents search_last / search_first as the API surface and shows custom strategies extending the FFF-owned functions — which also means third-party strategies no longer commit type piracy on Base. auto.md's per-query section described the v2 pick-at-every-query decision tree; it now shows the v3 construction-time kind resolution including the KIND_UNIFORM_STEP props path. Remaining searchsortedlast(strategy, ...) examples in index.md / guessers.md / equality.md renamed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Rename search_last/search_first → searchsorted_last/searchsorted_first The v3 dispatcher names dropped the 'sorted' cue that Base's searchsortedfirst/searchsortedlast carried — but these functions share that precondition exactly (v must be sorted; assumed, not checked). Put 'sorted' back in the name. The underscore form stays distinct from Base's exported searchsortedfirst/searchsortedlast (so it remains exportable and bare calls resolve to FFF's) and matches the existing FFF family searchsortedfirst!/searchsortedlast!/searchsortedrange. Mechanical rename of the public functions, their methods (Auto, GuesserHint, the struct fallback, the kind dispatchers), the internal _search_*_ dispatch helpers, all call sites, exports, tests, NEWS, and docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Document the sorted precondition on searchsorted_last/searchsorted_first State explicitly that v must be sorted ascending under order (assumed, not checked, as with Base.searchsortedlast) — the summary previously only implied it via the polarity reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Docs: drop 'v3 preferred' framing — the v2 path was removed, not deprecated 'Preferred' implied a lesser-but-available alternative (the v2 Base.searchsortedlast strategy methods), but those are removed. Reframe the strategies overview and the SearchStrategy docstring so the enum tag and the strategy struct read as two ways to call the single search API, not as 'preferred vs legacy'. Drop the stale '(v3 preferred path)' export comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> --------- Co-authored-by: Chris Rackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent aa8b03e commit 573a0b5

26 files changed

Lines changed: 2555 additions & 1642 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ bench/Manifest.toml
33
docs/build
44
docs/Manifest.toml
55
docs/src/assets/Manifest.toml
6-
docs/src/assets/Project.toml
6+
docs/src/assets/Project.toml
7+
test/qa/Manifest.toml

NEWS.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,161 @@
11
# FindFirstFunctions.jl NEWS
22

3+
## 3.0.0
4+
5+
The 2.x sorted-search API was a single generic
6+
`Base.searchsortedlast(::SearchStrategy, v, x[, hint])` /
7+
`Base.searchsortedfirst(::SearchStrategy, ...)` extended once per
8+
concrete strategy struct. That design made dispatch type-stable on the
9+
*chosen* strategy but produced `Union` returns whenever the strategy
10+
itself depended on runtime data — in particular `Auto`'s decision tree,
11+
where the strategy struct returned by `_auto_pick(v, hint)` had a
12+
runtime-dependent type, broke `@inferred`, and gave `Vector{Auto}`-style
13+
containers a `Union` element type.
14+
15+
The 3.0 redesign replaces the multimethod dispatch on `SearchStrategy`
16+
singletons with a single FFF-owned dispatcher tagged by an enum:
17+
18+
```julia
19+
searchsorted_last(KIND_BRACKET_GALLOP, v, x, hint)
20+
searchsorted_first(KIND_INTERPOLATION_SEARCH, v, x)
21+
```
22+
23+
The runtime `if/elseif` over `StrategyKind` values is well-predicted in
24+
hot loops, the kernel bodies inline, and the return path stays concrete
25+
(`Int`) regardless of which kind is picked at runtime. A benchmark sweep
26+
across 20 representative cells measured ~0 ns of overhead vs. the v2
27+
multimethod path.
28+
29+
### Breaking: the `Base.searchsortedlast(::S, ...)` API is removed
30+
31+
v3 no longer extends `Base.searchsortedlast` / `Base.searchsortedfirst`
32+
with strategy methods. The FFF-owned `searchsorted_last` / `searchsorted_first`
33+
dispatchers are the only search entry points; they accept a
34+
`StrategyKind` tag, a strategy struct (which forwards through
35+
[`strategy_kind`] and constant-folds for literal strategies), or a
36+
stateful strategy (`Auto`, `GuesserHint`):
37+
38+
| v2 (removed) | v3 |
39+
|---|---|
40+
| `searchsortedlast(BracketGallop(), v, x, hint)` | `searchsorted_last(KIND_BRACKET_GALLOP, v, x, hint)` or `searchsorted_last(BracketGallop(), v, x, hint)` |
41+
| `searchsortedfirst(InterpolationSearch(), v, x)` | `searchsorted_first(KIND_INTERPOLATION_SEARCH, v, x)` or `searchsorted_first(InterpolationSearch(), v, x)` |
42+
| `searchsortedlast(UniformStep(), r, x)` | `searchsorted_last(KIND_UNIFORM_STEP, r, x)` or `searchsorted_last(UniformStep(), r, x)` |
43+
| `searchsortedlast(Auto(v), v, x, hint)` | `searchsorted_last(Auto(v), v, x, hint)` |
44+
| `searchsortedfirst(GuesserHint(g), v, x)` | `searchsorted_first(GuesserHint(g), v, x)` |
45+
46+
(The same rename applies to every other singleton strategy.) The batched
47+
in-place API (`searchsortedlast!` / `searchsortedfirst!` /
48+
`searchsortedrange`) is FFF-owned and unchanged.
49+
50+
Stateful strategies (`Auto`, `GuesserHint`) stay on the multimethod path
51+
because they carry per-instance data.
52+
53+
### Breaking changes — `Auto` resolves at construction
54+
55+
In v2, `Auto()`'s per-query `Base.searchsortedlast(::Auto, v, x, hint)`
56+
ran the picking logic on every call (consulting `length(v)`, hint
57+
validity, and `props.is_uniform`). In v3, `Auto` carries a resolved
58+
`StrategyKind` and per-query dispatch is a one-line forward to
59+
`searchsorted_last(s.kind, v, x, hint)`:
60+
61+
- `Auto()` defaults to `KIND_BINARY_BRACKET` (safe; matches
62+
`Base.searchsortedlast` exactly).
63+
- `Auto(v)` resolves the kind from `length(v)` + `SearchProperties(v)`.
64+
Pre-pay the probe cost once, get the v2 fast-path on every per-query
65+
call afterwards.
66+
- `Auto(v, props)` is the same with a pre-computed `props` cache.
67+
68+
The **batched** Auto dispatch (`searchsortedlast!(out, v, queries;
69+
strategy = Auto())`) still re-resolves the kind from `(v, queries)`
70+
because the gap heuristic needs the queries; that decision tree is
71+
type-stable in v3 (returns a `StrategyKind`, dispatched via the enum
72+
switch into a kind-parameterized loop).
73+
74+
The v2 behaviour of `Auto()` re-picking on every per-query call is
75+
preserved for batched calls and for callers that explicitly construct
76+
`Auto(v)` per query. For callers that previously relied on `Auto()` (no
77+
`v`) picking `LinearScan` on short vectors or `BracketGallop` on long
78+
vectors per-query, update to `Auto(v)` where `v` is known.
79+
80+
### New: parametric `SearchProperties{T}` with precomputed `inv_step`
81+
82+
`SearchProperties` is now parametric on the data ratio type
83+
`T = typeof(oneunit(eltype(v)) / oneunit(eltype(v)))` (`Float64` for
84+
`Vector{Int}` or `Vector{Float64}`, `Float32` for `Vector{Float32}`, etc.).
85+
The struct carries two new fields:
86+
87+
- `first_val::T``v[1]` (or `first(r)` for an `AbstractRange`).
88+
- `inv_step::T` — the precomputed reciprocal `1 / step`. For an
89+
`AbstractRange`, `1 / step(r)`. For an exactly-uniform `AbstractVector`,
90+
`(length(v) - 1) / (v[end] - v[1])`.
91+
92+
These fields are populated when `is_uniform = true` and zero otherwise.
93+
94+
For `AbstractVector` data, `is_uniform` is detected by the cheap 11-point
95+
sampled pre-filter and then confirmed by an exact O(n) scan over every
96+
element. The exact pass matters: data that is uniform at the sampled
97+
points but jittered between them would otherwise be flagged uniform, and
98+
the closed-form lookup would land in the wrong cell. The kernels also
99+
carry a correction walk, so even a caller-forced `is_uniform = true` on
100+
non-uniform data degrades to a slower search rather than a wrong answer.
101+
102+
The populated fields feed the new **props-aware `UniformStep` kernel**
103+
invoked by `Auto(v)` when the resolved kind is `KIND_UNIFORM_STEP`:
104+
105+
```julia
106+
# v3 closed-form O(1) lookup with no per-query division:
107+
a = Auto(0.0:0.5:100.0) # kind = KIND_UNIFORM_STEP, inv_step = 2.0
108+
searchsorted_last(a, r, 3.7) # → floor((3.7 - 0.0) * 2.0) + 1 = 8
109+
```
110+
111+
This subsumes the never-merged `DirectStep` strategy (PR #74) — its
112+
precomputed-reciprocal closed-form is now folded into `UniformStep` via
113+
the `SearchProperties{T}` payload.
114+
115+
The raw `UniformStep()` singleton kept its old behaviour: when called via
116+
`searchsortedlast(UniformStep(), r, x)` (no props) it still uses
117+
`fld(diff, step)` per query. Auto routes through the props-aware kernel
118+
because Auto carries the precomputed `SearchProperties{T}`.
119+
120+
### `Auto{T}` parametric
121+
122+
`Auto` now carries `props::SearchProperties{T}` and is itself parametric
123+
on `T`. `Auto(v)` returns an `Auto{T}` where `T` is the ratio type of
124+
`eltype(v)`. Two `Auto`s constructed from data with the same ratio type
125+
(e.g. `Vector{Int}` and `Vector{Float64}` both → `Auto{Float64}`) share
126+
a single concrete type, so `Vector{Auto{Float64}}` is concrete.
127+
128+
### New: `strategy_kind`
129+
130+
`strategy_kind(s::SearchStrategy)` maps a singleton strategy struct to
131+
its `StrategyKind` tag, and returns the stored kind for `Auto`.
132+
`GuesserHint` (genuinely stateful, no singleton tag) throws
133+
`ArgumentError`.
134+
135+
### `findequal` now accepts a `StrategyKind` directly
136+
137+
```julia
138+
findequal(KIND_BRACKET_GALLOP, v, x)
139+
findequal(KIND_BRACKET_GALLOP, v, x, hint)
140+
```
141+
142+
In addition to the `findequal(strategy_struct, v, x[, hint])` form,
143+
which forwards through the same struct → kind mapping.
144+
145+
### Internals — `dispatch.jl` split into `kinds.jl` + `kernels.jl` + `strategy_kind.jl`
146+
147+
The v2 `src/dispatch.jl` file (Base.searchsortedlast extensions per
148+
strategy) is gone. In its place:
149+
150+
- `src/kinds.jl``StrategyKind` enum and `searchsorted_last` /
151+
`searchsorted_first` enum dispatchers.
152+
- `src/kernels.jl` — per-strategy kernel functions
153+
(`_kernel_last_bracket_gallop`, etc.), lifted out of the v2 method
154+
bodies.
155+
- `src/strategy_kind.jl` — the struct → kind mapping plus the
156+
struct-valued `searchsorted_last(::S, ...)` / `searchsorted_first(::S, ...)`
157+
entry points that forward through it.
158+
3159
## 2.0.0
4160

5161
This is a major rewrite of the sorted-search API. The 1.x surface — a

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "FindFirstFunctions"
22
uuid = "64ca27bc-2ba2-4a57-88aa-44e436879224"
3-
version = "2.1.0"
3+
version = "3.0.0"
44
authors = ["Chris Elrod <elrodc@gmail.com> and contributors"]
55

66
[deps]

bench/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
77

88
[compat]
99
BenchmarkTools = "1.8.0"
10-
FindFirstFunctions = "2.1.0"
10+
FindFirstFunctions = "3"
1111
PrettyTables = "3.3.2"
1212
StableRNGs = "1.0.4"
1313
Statistics = "1.11.1"

docs/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
1010
[compat]
1111
DataInterpolations = "4, 5, 6, 7, 8"
1212
Documenter = "1"
13-
FindFirstFunctions = "1.4.2, 2.0"
13+
FindFirstFunctions = "1.4.2, 2.0, 3.0"
1414
Optim = "1, 2.0"
1515
Plots = "1"
1616
RegularizationTools = "0.6"

docs/src/auto.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ script at the end of the page generates the comparison grid.
1010

1111
The decision differs between per-query and batched callers.
1212

13-
### Per-query: `searchsortedlast(Auto(), v, x[, hint])`
13+
### Per-query: `searchsorted_last(Auto(v), v, x[, hint])`
14+
15+
The kind is resolved once, at construction, and every per-query call
16+
forwards to it:
1417

1518
```
16-
hint missing or out of axes(v) → BinaryBracket
17-
length(v) ≤ 16 → LinearScan # _AUTO_LINEAR_THRESHOLD
18-
otherwise → BracketGallop
19+
Auto() (no data) → KIND_BINARY_BRACKET
20+
Auto(v) and props.is_uniform → KIND_UNIFORM_STEP # props-aware closed form
21+
Auto(v) and length(v) ≤ 16 → KIND_LINEAR_SCAN # _AUTO_LINEAR_THRESHOLD
22+
Auto(v) otherwise → KIND_BRACKET_GALLOP
1923
```
2024

2125
`Auto` never picks `InterpolationSearch` or `ExpFromLeft` in the per-query

docs/src/equality.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ FindFirstFunctions.findfirstsortedequal
3737
| Does `x` occur in this *unsorted* vector? | any | [`findfirstequal`](@ref FindFirstFunctions.findfirstequal) |
3838
| Does `x` occur in this *sorted* vector? | `DenseVector{Int64}` + `Int64` | [`findfirstsortedequal`](@ref FindFirstFunctions.findfirstsortedequal) (or `findequal(BisectThenSIMD(), v, x)` for the sentinel-returning variant) |
3939
| Does `x` occur in this *sorted* vector? | other eltypes | [`findequal`](@ref FindFirstFunctions.findequal) with any strategy |
40-
| Where would `x` insert into this sorted vector? | any | `searchsortedfirst(strategy, v, x[, hint])` |
40+
| Where would `x` insert into this sorted vector? | any | `searchsorted_first(strategy, v, x[, hint])` |
4141

4242
## SIMD primitives
4343

docs/src/guessers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ v = collect(0.0:0.1:10.0)
7272
g = Guesser(v)
7373
strat = GuesserHint(g)
7474

75-
i = searchsortedlast(strat, v, 3.14)
75+
i = searchsorted_last(strat, v, 3.14)
7676
@assert g.idx_prev[] == i # the guesser caches the last result
7777
```
7878

@@ -93,7 +93,7 @@ end
9393
Interp(v) = Interp(v, Guesser(v))
9494

9595
function find_segment(itp::Interp, x)
96-
return searchsortedlast(GuesserHint(itp.g), itp.v, x)
96+
return searchsorted_last(GuesserHint(itp.g), itp.v, x)
9797
end
9898
```
9999

docs/src/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ v = collect(0.0:0.1:10.0)
5151
queries = sort!(rand(100) .* 10)
5252

5353
# Single query with a hint.
54-
i = searchsortedlast(BracketGallop(), v, 3.14, 30)
54+
i = searchsorted_last(BracketGallop(), v, 3.14, 30)
5555

5656
# Batched, with strategy chosen by Auto.
5757
idx = Vector{Int}(undef, length(queries))

0 commit comments

Comments
 (0)