Skip to content

Commit 441cfd5

Browse files
authored
Merge pull request #30 from akashsoni01/main
2.0.6 and iter
2 parents 943e82b + 9a98f27 commit 441cfd5

69 files changed

Lines changed: 11059 additions & 1153 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/rust.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Rust
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
jobs:
13+
build:
14+
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
- name: Build
20+
run: cargo build --verbose
21+
- name: Run tests
22+
run: cargo test --verbose

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ key-paths-core/.idea/tagged-core.iml
3232
key-paths-core/.idea/modules.xml
3333
key-paths-core/.idea/key-paths-core.iml
3434
*.mdc
35+
36+
# Cheque dataset (benchmark images)
37+
benches/cheq_dataset

Cargo.toml

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rust-key-paths"
3-
version = "2.0.0"
3+
version = "2.0.7"
44
edition = "2024"
55
authors = ["Codefonsi <info@codefonsi.com>"]
66
license = "MPL-2.0"
@@ -14,19 +14,26 @@ include = ["src/**/*", "Cargo.toml", "README.md", "LICENSE"]
1414

1515
[dependencies]
1616
async-trait = "0.1"
17-
tokio = { version = "1.38.0", features = ["sync", "rt"], optional = true }
17+
pin-project = { version = "1.1", optional = true }
18+
tokio = { version = "1.38.0", features = ["sync", "rt", "rt-multi-thread", "macros", "time"], optional = true }
1819
parking_lot = { version = "0.12", optional = true }
1920

2021
[workspace]
2122
resolver = "3" # or "3"
2223
members = [
2324
"key-paths-derive",
25+
"key-paths-iter",
2426
]
2527

28+
# Use local rust-key-paths when developing (remove or override when publishing dependents)
29+
[patch.crates-io]
30+
rust-key-paths = { path = "." }
31+
2632

2733
[features]
2834
default = []
2935
parking_lot = ["dep:parking_lot"]
36+
pin_project = ["dep:pin-project"]
3037
tagged_core = ["tagged-core/default"]
3138
tokio = ["dep:tokio"]
3239

@@ -40,6 +47,10 @@ uuid = { version = "1.0", features = ["v4", "serde"] }
4047
criterion = { version = "0.5", features = ["html_reports"] }
4148
tokio = { version = "1.38.0", features = ["sync", "rt", "rt-multi-thread", "macros"] }
4249
key-paths-derive = { path = "key-paths-derive" }
50+
key-paths-iter = { path = "key-paths-iter", features = ["rayon", "gpu"] }
51+
num_cpus = "1.16"
52+
rayon = "1.10"
53+
pin-project = "1.1"
4354

4455
[[bench]]
4556
name = "deep_chain_leaf"
@@ -56,3 +67,27 @@ harness = false
5667
[[bench]]
5768
name = "deep_chain_async_only"
5869
harness = false
70+
71+
[[bench]]
72+
name = "keypath_vs_unwrap"
73+
harness = false
74+
75+
[[bench]]
76+
name = "ten_level_arc_rwlock"
77+
harness = false
78+
79+
[[bench]]
80+
name = "ten_level_std_rwlock"
81+
harness = false
82+
83+
[[bench]]
84+
name = "ten_level_tokio_rwlock"
85+
harness = false
86+
87+
[[bench]]
88+
name = "scale_par_bench"
89+
harness = false
90+
91+
[[bench]]
92+
name = "akp_cpu_bench"
93+
harness = false

README.md

Lines changed: 252 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,265 @@
33
Key paths provide a **safe, composable way to access and modify nested data** in Rust.
44
Inspired by **KeyPath and Functional Lenses** system, this feature rich crate lets you work with **struct fields** and **enum variants** as *first-class values*.
55

6+
## Starter Guide
7+
8+
### Installation
9+
10+
Add to your `Cargo.toml`:
11+
12+
```toml
13+
[dependencies]
14+
rust-key-paths = "2.0.6"
15+
key-paths-derive = "2.0.6"
16+
```
17+
18+
### Basic usage
19+
20+
```rust
21+
use std::sync::Arc;
22+
use key_paths_derive::Kp;
23+
24+
#[derive(Debug, Kp)]
25+
struct SomeComplexStruct {
26+
scsf: Option<SomeOtherStruct>,
27+
scfs2: Arc<std::sync::RwLock<SomeOtherStruct>>,
28+
}
29+
30+
#[derive(Debug, Kp)]
31+
struct SomeOtherStruct {
32+
sosf: Option<OneMoreStruct>,
33+
}
34+
35+
#[derive(Debug, Kp)]
36+
enum SomeEnum {
37+
A(String),
38+
B(Box<DarkStruct>),
39+
}
40+
41+
#[derive(Debug, Kp)]
42+
struct OneMoreStruct {
43+
omsf: Option<String>,
44+
omse: Option<SomeEnum>,
45+
}
46+
47+
#[derive(Debug, Kp)]
48+
struct DarkStruct {
49+
dsf: Option<String>,
50+
}
51+
52+
impl SomeComplexStruct {
53+
fn new() -> Self {
54+
Self {
55+
scsf: Some(SomeOtherStruct {
56+
sosf: Some(OneMoreStruct {
57+
omsf: Some(String::from("no value for now")),
58+
omse: Some(SomeEnum::B(Box::new(DarkStruct {
59+
dsf: Some(String::from("dark field")),
60+
}))),
61+
}),
62+
}),
63+
scfs2: Arc::new(std::sync::RwLock::new(SomeOtherStruct {
64+
sosf: Some(OneMoreStruct {
65+
omsf: Some(String::from("no value for now")),
66+
omse: Some(SomeEnum::B(Box::new(DarkStruct {
67+
dsf: Some(String::from("dark field")),
68+
}))),
69+
}),
70+
})),
71+
}
72+
}
73+
}
74+
fn main() {
75+
let mut instance = SomeComplexStruct::new();
76+
77+
SomeComplexStruct::scsf()
78+
.then(SomeOtherStruct::sosf())
79+
.then(OneMoreStruct::omse())
80+
.then(SomeEnum::b())
81+
.then(DarkStruct::dsf())
82+
.get_mut(&mut instance).map(|x| {
83+
*x = String::from("🖖🏿🖖🏿🖖🏿🖖🏿");
84+
});
85+
86+
println!("instance = {:?}", instance.scsf.unwrap().sosf.unwrap().omse.unwrap());
87+
// output - instance = B(DarkStruct { dsf: Some("🖖🏿🖖🏿🖖🏿🖖🏿") })
88+
}
89+
```
90+
91+
### Composing keypaths
92+
93+
Chain through nested structures with `then()`:
94+
95+
```rust
96+
#[derive(Kp)]
97+
struct Address { street: String }
98+
99+
#[derive(Kp)]
100+
struct Person { address: Box<Address> }
101+
102+
let street_kp = Person::address().then(Address::street());
103+
let street = street_kp.get(&person); // Option<&String>
104+
```
105+
106+
### Partial and Any keypaths
107+
108+
Use `#[derive(Pkp, Akp)]` (requires `Kp`) to get type-erased keypath collections:
109+
110+
- **PKp**`partial_kps()` returns `Vec<PKp<Self>>`; value type erased, root known
111+
- **AKp**`any_kps()` returns `Vec<AKp>`; both root and value type-erased for heterogeneous collections
112+
113+
Filter by `value_type_id()` / `root_type_id()` and read with `get_as()`. For writes, dispatch to the typed `Kp` (e.g. `Person::name()`) based on TypeId.
114+
115+
See examples: `pkp_akp_filter_typeid`, `pkp_akp_read_write_convert`.
116+
117+
### GPU / wgpu (key-paths-iter, optional)
118+
119+
The [key-paths-iter](https://github.com/codefonsi/rust-key-paths) crate can run **numeric** keypaths (e.g. `f32`) on the GPU via wgpu. Two styles:
120+
121+
- **AKp runner** (`wgpu` module): `IntoNumericAKp` from Kp, `AKpTier::Numeric` / `Arbitrary`, `AKpRunner`. Examples: `kp_pkp_wgpu`, `akp_wgpu_runner`.
122+
- **Kp-only** (`kp_gpu` module): no AKp/PKp — `.map_gpu(wgsl)`, `.par_gpu(wgsl, roots, ctx)`, `GpuKpRunner`. Examples: `kp_gpu_example`, `kp_gpu_vec_example`, `kp_gpu_practical_app` (finance: Monte Carlo, batch options, stress-test). Kp with value `Vec<V>`: `.map_gpu_vec(wgsl)` for one dispatch over the vector.
123+
124+
Run benchmarks: `cargo bench --bench akp_cpu_bench`. Typical results (MacBook Air M1):
125+
126+
| Roots | Serial (CPU) | Parallel CPU (Rayon) | Parallel GPU (wgpu) |
127+
|--------|---------------|----------------------|---------------------|
128+
| 1,000 | ~35 µs | ~86 µs | ~1.6 ms |
129+
| 10,000 | ~350 µs | ~425 µs | ~1.9 ms |
130+
| 50,000 | ~1.8 ms | ~1.8 ms | ~3.7 ms |
131+
| 100,000| ~3.7 ms | ~3.7 ms | ~5.5 ms |
132+
133+
For this lightweight transform, CPU wins; GPU pays off for larger batches or heavier per-element math.
134+
135+
### Features
136+
137+
| Feature | Description |
138+
|---------|-------------|
139+
| `parking_lot` | Use `parking_lot::Mutex` / `RwLock` instead of `std::sync` |
140+
| `tokio` | Async lock support (`tokio::sync::Mutex`, `RwLock`) |
141+
| `pin_project` | Enable `#[pin]` field support for pin-project compatibility |
142+
143+
### More examples
144+
145+
```bash
146+
cargo run --example kp_derive_showcase
147+
cargo run --example pkp_akp_filter_typeid
148+
cargo run --example pkp_akp_read_write_convert
149+
# Kp/Pkp + wgpu (key-paths-iter with gpu feature)
150+
cargo run --example kp_pkp_wgpu
151+
cargo run --example akp_wgpu_runner
152+
cargo run --example kp_gpu_example
153+
cargo run --example kp_gpu_vec_example
154+
cargo run --example kp_gpu_practical_app
155+
# Box and Pin support
156+
cargo run --example box_and_pin_example
157+
# pin_project #[pin] fields
158+
cargo run --example pin_project_example --features pin_project
159+
cargo run --example pin_project_fair_race --features "pin_project,tokio"
160+
# Deadlock prevention (parallel execution)
161+
cargo run --example deadlock_prevention_sync --features parking_lot
162+
cargo run --example deadlock_prevention_async --features tokio
163+
```
164+
165+
---
166+
167+
## Supported containers
168+
169+
The `#[derive(Kp)]` macro (from `key-paths-derive`) generates keypath accessors for these wrapper types:
170+
171+
| Container | Access | Notes |
172+
|-----------|--------|-------|
173+
| `Option<T>` | `field()` | Unwraps to inner type |
174+
| `Box<T>` | `field()` | Derefs to inner |
175+
| `Pin<T>`, `Pin<Box<T>>` | `field()`, `field_inner()` | Container + inner (when `T: Unpin`) |
176+
| `Rc<T>`, `Arc<T>` | `field()` | Derefs; mut when unique ref |
177+
| `Vec<T>` | `field()`, `field_at(i)` | Container + index access |
178+
| `HashMap<K,V>`, `BTreeMap<K,V>` | `field_at(k)` | Key-based access |
179+
| `HashSet<T>`, `BTreeSet<T>` | `field()` | Container identity |
180+
| `VecDeque<T>`, `LinkedList<T>`, `BinaryHeap<T>` | `field()`, `field_at(i)` | Index where applicable |
181+
| `Result<T,E>` | `field()` | Unwraps `Ok` |
182+
| `Cow<'_, T>` | `field()` | `as_ref` / `to_mut` |
183+
| `Option<Cow<'_, T>>` | `field()` | Optional Cow unwrap |
184+
| `std::sync::Mutex<T>`, `std::sync::RwLock<T>` | `field()` | Container (use `LockKp` for lock-through) |
185+
| `Arc<Mutex<T>>`, `Arc<RwLock<T>>` | `field()`, `field_lock()` | Lock-through via `LockKp` |
186+
| `tokio::sync::Mutex`, `tokio::sync::RwLock` | `field_async()` | Async lock-through (tokio feature) |
187+
| `parking_lot::Mutex`, `parking_lot::RwLock` | `field()`, `field_lock()` | parking_lot feature |
188+
189+
Nested combinations (e.g. `Option<Box<T>>`, `Option<Vec<T>>`, `Vec<Option<T>>`) are supported.
190+
191+
### pin_project `#[pin]` fields (optional feature)
192+
193+
When using [pin-project](https://docs.rs/pin-project), mark pinned fields with `#[pin]`. The derive generates:
194+
195+
| `#[pin]` field type | Access | Notes |
196+
|---------------------|--------|-------|
197+
| Plain (e.g. `i32`) | `field()`, `field_pinned()` | Pinned projection via `this.project()` |
198+
| `Future` | `field()`, `field_pinned()`, `field_await()` | Poll through `Pin<&mut Self>` |
199+
| `Box<dyn Future<Output=T>>` | `field()`, `field_pinned()`, `field_await()` | Same for boxed futures |
200+
201+
Enable with `pin_project` feature and add `#[pin_project]` to your struct:
202+
203+
```rust
204+
#[pin_project]
205+
#[derive(Kp)]
206+
struct WithPinnedFuture {
207+
fair: bool,
208+
#[pin]
209+
fut: Pin<Box<dyn Future<Output = String> + Send>>,
210+
}
211+
```
212+
213+
Examples: `pin_project_example`, `pin_project_fair_race` (FairRaceFuture use case).
214+
215+
## Performance: Kp vs direct unwrap
216+
217+
Benchmark: nested `Option` chains and enum case paths (`cargo bench --bench keypath_vs_unwrap`).
218+
219+
| Scenario | Keypath | Direct unwrap | Overhead |
220+
|----------|---------|---------------|----------|
221+
| 100× reuse (3-level) | ~36.6 ns | ~36.7 ns | ~1x |
222+
| 100× reuse (5-level) | ~52.3 ns | ~52.5 ns | ~1x |
223+
224+
Access overhead comes from closure indirection in the composed chain. **Reusing a keypath** (build once, use many times) matches direct unwrap; building the chain each time adds ~1–2 ns.
225+
226+
### Would static keypaths help?
227+
228+
Yes. Static/const keypaths would:
229+
- Remove creation cost entirely (no closure chain construction per use)
230+
- Allow the compiler to inline the full traversal
231+
- Likely close the gap to near-zero overhead vs manual unwrap
232+
233+
Currently, `Kp::then()` composes via closures that capture the previous step, so each access goes through a chain of function calls. A static keypath could flatten this to direct field offsets.
234+
235+
---
236+
237+
## Performance: LockKp (Arc&lt;Mutex&gt;, Arc&lt;RwLock&gt;)
238+
6239
| Operation | Keypath | Direct Locks | Overhead |
7240
|-----------|---------|--------------|----------|
8241
| **Read** | ~241 ns | ~117 ns | ~2.1x |
9242
| **Write** | ~239 ns | ~114 ns | ~2.1x |
10243

11-
The keypath approach builds the chain each iteration and traverses through `LockKp.then().then().then_async().then()`; direct locks use `sync_mutex.lock()` then `tokio_mutex.lock().await`. The keypath overhead reflects chain construction plus composed traversal vs. manual lock nesting. Hot-path functions are annotated with `#[inline]` for improved performance.
244+
The keypath approach builds the chain each iteration and traverses through `LockKp.then().then().then_async().then()`; direct locks use `sync_mutex.lock()` then `tokio_mutex.lock().await`. Hot-path functions are annotated with `#[inline]` for improved performance.
245+
246+
### 10-level deep Arc&lt;RwLock&gt; benchmarks (leaf: f64)
247+
248+
Benchmark: 10 levels of nested `Arc<RwLock<Next>>`, reading/writing leaf `f64`. Run with:
249+
- `cargo bench --features parking_lot --bench ten_level_arc_rwlock`
250+
- `cargo bench --bench ten_level_std_rwlock`
251+
- `cargo bench --features tokio --bench ten_level_tokio_rwlock`
252+
253+
**Incr** (write: leaf += 0.25):
254+
255+
| RwLock implementation | keypath_static | keypath_dynamic | direct_lock |
256+
|-----------------------|----------------|-----------------|-------------|
257+
| **parking_lot** | ~34 ns | ~41 ns | ~39 ns |
258+
| **std::sync** | ~46 ns | ~54 ns | ~46 ns |
259+
| **tokio::sync** | ~1.79 µs | ~1.78 µs | ~278 ns |
260+
261+
Static keypath (chain built once, reused) matches or beats direct lock for sync RwLocks. For tokio, async keypath has higher overhead than direct `.read().await`/`.write().await`; direct lock is fastest.
12262

13263
---
14264

15265
## 📜 License
16266

17-
* Mozilla Public License 2.0
267+
* Mozilla Public License 2.0

0 commit comments

Comments
 (0)