diff --git a/README.md b/README.md index 732dece..c4dcaed 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ via two complementary mechanisms: | Mechanism | Scope | Enforcement | Description | |------------|----------------|-----------------|-----------------------------------------------------------------| | `LockSet` | Within a level | By construction | Atomic multi-lock acquisition, sorted by monotonic `LockId` | -| Levels | Across levels | Compile-time | `LockAfter` trait bounds on a consumed-and-re-emitted `LockKey` | +| Levels | Across levels | Compile-time | `LockAfter` trait bounds on a consumed-and-re-emitted `MutexKey` | Every lock call is infallible or doesn't compile. No `Result`, no `Option`, no panic on any lock acquisition path. @@ -21,7 +21,7 @@ no `Option`, no panic on any lock acquisition path. | Crate | Description | |----------------------------------------|-----------------------------------------------------------| -| [`surelock`](./surelock) | Core library -- `Mutex`, `LockSet`, `LockKey`, `Lockable` | +| [`surelock`](./surelock) | Core library -- `Mutex`, `LockSet`, `MutexKey`, `Lockable` | | [`surelock_macros`](./surelock_macros) | Proc macros -- `lock_order!` for level orderings | See [`surelock/README.md`](./surelock/README.md) for usage examples diff --git a/surelock/README.md b/surelock/README.md index b8ac399..5a01c5e 100644 --- a/surelock/README.md +++ b/surelock/README.md @@ -8,7 +8,7 @@ Surelock prevents deadlocks by breaking the _circular-wait_ | Mechanism | Scope | Enforcement | Description | |------------|----------------|-----------------|-----------------------------------------------------------------| | `LockSet` | Within a level | By construction | Atomic multi-lock acquisition, sorted by monotonic `LockId` | -| Levels | Across levels | Compile-time | `LockAfter` trait bounds on a consumed-and-re-emitted `LockKey` | +| Levels | Across levels | Compile-time | `LockAfter` trait bounds on a consumed-and-re-emitted `MutexKey` | Every lock call is infallible or doesn't compile. No `Result`, no `Option`, no panic on any lock acquisition path. @@ -18,7 +18,7 @@ no `Option`, no panic on any lock acquisition path. Surelock's two mechanisms address different granularities of the deadlock problem -- and they don't overlap. -### `LockSet`: Fine-Grained, Ordered +### `LockSet`: Fine-Grained, Implicitly Ordered When you need multiple locks at the _same level_ -- say, two accounts in a transfer -- `LockSet` handles it. Each `Mutex` gets a unique, @@ -56,15 +56,15 @@ lock_order! { Config => Account => Transaction } Levels are deliberately coarse. You don't assign a level to each _instance_ -- you assign one to each _category_ of lock. "Config locks come before account locks" is a system-wide invariant, not a -per-instance decision. The `LockKey` type parameter tracks which +per-instance decision. The `MutexKey` type parameter tracks which level you've reached, and the compiler rejects any acquisition that would go backwards. ``` -lock_scope(|key| // key: LockKey<'_, Bottom> - let (cfg, key) = key.lock(&c); // key: LockKey<'_, Config> - let (acct, key) = key.lock(&a); // key: LockKey<'_, Account> - let (tx, _key) = key.lock(&t); // key: LockKey<'_, Transaction> +lock_scope(|key| // key: MutexKey<'_, Bottom> + let (cfg, key) = key.lock(&c); // key: MutexKey<'_, Config> + let (acct, key) = key.lock(&a); // key: MutexKey<'_, Account> + let (tx, _key) = key.lock(&t); // key: MutexKey<'_, Transaction> // key.lock(&c2) --> compile error: Config is not LockAfter ); @@ -104,6 +104,39 @@ by `LockId`). Across levels, the type system handles category ordering (enforced by `LockAfter` bounds on the key). Neither mechanism needs to know about the other. +### Type Lifecycle + +``` + ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ + │ Thread Implicit │ + │ std (default) │ + │ │ + │ KeyCell ─────────────────────┐ │ + │ (claim) │ │ + └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ┘ + │ + ▼ + MutexKey ───▶ MutexGuard + (scope) (access) + ▲ + ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ┐ + │ Explicit │ │ + │ e.g. for no_std multi-core │ │ + │ │ │ + │ Locksmith ──▶ KeyVoucher ────┘ │ + │ (forge) (deliver) │ + └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ + + lock_scope / try_lock_scope are ambient + helpers that use KeyCell internally. +``` + +Most users interact with `MutexKey` (via `lock_scope` or +`KeyCell::grant`) and `MutexGuard`. The `Locksmith` and +`KeyVoucher` types are for explicit `no_std` multi-core key +distribution (v2). The two entry mechanisms are mutually +exclusive on a given thread. + ## Quick Start ```rust @@ -207,65 +240,162 @@ lock_scope(|key| { }); ``` -## `no_std` +## Scope Entry: Ambient vs Capability-Based -Surelock is `no_std + alloc` by default. The core library has no -dependency on `std` -- just swap in a `no_std`-compatible -`RawMutex` backend: +Surelock offers two ways to enter a lock scope. Both produce +the same `MutexKey` -- the difference is how nesting is prevented. -```toml -[dependencies] -surelock = "0.1" -spin = { version = "0.9", features = ["lock_api", "spin_mutex"] } ``` + Ambient (lock_scope / try_lock_scope) + ┌──────────────────────────────────────────────┐ + │ Convenient: call from anywhere │ + │ Nesting check: runtime (thread_local! flag) │ + │ no_std: no check (documented hazard) │ + └──────────────────────────────────────────────┘ + + Capability-based (LockAuthority) + ┌──────────────────────────────────────────────┐ + │ Explicit: thread the authority via &mut │ + │ Nesting check: static (&mut borrow checker) │ + │ no_std: static nesting prevention (no flag!) │ + └──────────────────────────────────────────────┘ +``` + +### Ambient Entry + +The simplest path. Call `lock_scope` or `try_lock_scope` from +anywhere -- no setup required: + +```rust +// Panics if nested (application code, top-level) +lock_scope(|key| { ... }); + +// Returns None if nested (library code) +try_lock_scope(|key| { ... }); +``` + +On `std`, a `thread_local!` flag prevents nesting at runtime. + +### Capability-Based Entry with `LockAuthority` + +`LockAuthority` is a per-thread capability that governs scope +creation. It claims the thread's scope flag on construction and +holds it for its lifetime. `grant(&mut self)` takes a mutable +borrow, so the borrow checker prevents nesting at compile time: ```rust -use surelock::mutex::Mutex; +use surelock::lock_authority::LockAuthority; + +let mut auth = LockAuthority::new(); + +auth.grant(|key| { + let (guard, _key) = key.lock(&set); + // ... +}); + +// Sequential grants are fine -- &mut is released between calls +auth.grant(|key| { + // fresh scope, new key +}); + +// Nesting is a compile error: +// auth.grant(|key1| { +// auth.grant(|key2| { ... }); +// ^^^^ error: already mutably borrowed +// }); +``` + +While a `LockAuthority` exists, `try_lock_scope` returns `None` +on the same thread -- this prevents mixing the two entry mechanisms +and accidentally creating independent keys. -type SpinMutex = Mutex, T>; +Each thread creates its own authority. The authority is `!Send` -- +it governs _one_ thread's scope creation. Cross-thread deadlock +prevention comes from `LockSet`'s sorted acquisition order: -let counter: SpinMutex = Mutex::new(0); ``` + Thread A Thread B Thread C + ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ + │ LockAuthority │ │ lock_scope() │ │ LockAuthority │ + │ .grant() │ │ (ambient) │ │ .grant() │ + │ ▼ │ │ ▼ │ │ ▼ │ + │ MutexKey │ │ MutexKey │ │ MutexKey │ + │ ▼ │ │ ▼ │ │ ▼ │ + │ key.lock() │ │ key.lock() │ │ key.lock() │ + └──────┬────────┘ └──────┬────────┘ └──────┬────────┘ + └───────────────────────┼───────────────────────┘ + ▼ + Shared mutexes (Arc>) + LockSet sorts by LockId + → same order on every thread + → no deadlock +``` + +### `no_std` Nesting Prevention + +On `no_std`, there's no `thread_local!` -- so the ambient entry +points (`lock_scope` / `try_lock_scope`) have no nesting check. -Enable the `std` feature for the `thread_local!` scope uniqueness -check (prevents nested `lock_scope` calls on the same thread). On -`no_std`, this check is unavailable -- use `key.subscope()` for -safe nesting instead. +`LockAuthority` is the solution. Its `grant(&mut self)` provides +_static_ nesting prevention via the borrow checker, with no +runtime support required: + +```rust +// no_std: LockAuthority still prevents nesting at compile time +let mut auth = LockAuthority::new(); +auth.grant(|key| { + // auth.grant(|key2| { ... }); // compile error: &mut borrow + key.subscope(|inner_key| { ... }); // this is fine +}); +``` + +For nested locking _within_ a scope, use `key.subscope()` on all +targets -- it inherits the outer key's level and is compile-time +safe. ## Backend Agnostic -`Mutex` is generic over any [`lock_api::RawMutex`][lock_api] -implementation. Surelock doesn't include a lock implementation -- -bring your own: +`Mutex` is generic over any +[`raw_mutex::RawMutex`](crate::raw_mutex::RawMutex) +implementation. The `R` parameter defaults to `StdMutex` (wrapping +`std::sync::Mutex`) when the `std` feature is enabled. For +`no_std` or for `lock_api`-compatible backends like `parking_lot` +or `spin`, enable the `lock-api` feature: ```toml [dependencies] +# std users (default -- just works, Mutex uses StdMutex) surelock = "0.1" -# std (recommended for most users) +# lock_api users (parking_lot, spin, etc.) +surelock = { version = "0.1", features = ["lock-api"] } parking_lot = "0.12" -# or no_std -# spin = { version = "0.9", features = ["lock_api", "spin_mutex"] } +# no_std users +surelock = { version = "0.1", default-features = false, features = ["lock-api"] } +spin = { version = "0.9", features = ["lock_api", "spin_mutex"] } ``` ## Feature Flags -| Feature | Default | Description | -|----------------|---------|----------------------------------------------------------| -| `std` | no | `thread_local!` scope uniqueness check in `lock_scope()` | -| `escape-hatch` | no | `Mutex::unchecked_lock()` -- std-like direct lock | +| Feature | Default | Description | +|----------------|---------|-------------------------------------------------------------| +| `std` | yes | `StdMutex` default backend, `thread_local!` scope check | +| `lock-api` | no | Blanket `RawMutex` impl for any `lock_api::RawMutex` backend | +| `escape-hatch` | no | `Mutex::unchecked_lock()` -- std-like direct lock | ## Modules -| Module | Contents | -|------------|-------------------------------------------| -| `id` | `LockId` -- monotonic global counter | -| `key` | `LockKey`, `lock_scope()` | -| `level` | `Level`, `LockAfter`, `Base`, `Bottom` | -| `lockable` | `Lockable` trait + impls (single, tuples) | -| `mutex` | `Mutex`, `MutexGuard` | -| `set` | `LockSet` -- pre-sorted lock collection | +| Module | Contents | +|------------------|------------------------------------------------| +| `id` | `LockId` -- monotonic global counter | +| `key` | `MutexKey`, `lock_scope()`, `try_lock_scope()` | +| `level` | `Level`, `LockAfter`, `Base`, `Bottom` | +| `lock_authority` | `LockAuthority` -- capability-based scope entry | +| `lockable` | `Lockable` trait + impls (single, tuples) | +| `mutex` | `Mutex`, `MutexGuard` | +| `raw_mutex` | `RawMutex` trait, `StdMutex`, `lock_api` adapter | +| `set` | `LockSet` -- pre-sorted lock collection | ## Prior Art diff --git a/surelock/src/key.rs b/surelock/src/key.rs index 0435de5..67a2a56 100644 --- a/surelock/src/key.rs +++ b/surelock/src/key.rs @@ -1,7 +1,7 @@ //! Scope token for ordered lock acquisition. //! -//! [`LockKey`] tracks the current lock level as a type parameter. -//! It is consumed by [`LockKey::lock`] and re-emitted at the new level. +//! [`MutexKey`] tracks the current lock level as a type parameter. +//! It is consumed by [`MutexKey::lock`] and re-emitted at the new level. //! The key is `!Send + !Sync` and branded with an invariant lifetime //! to prevent escape from the [`lock_scope`] closure. @@ -13,15 +13,17 @@ use crate::{ set::LockSet, }; +extern crate alloc; + /// Scope token for ordered lock acquisition. /// /// Tracks the current lock level as a type parameter `Lvl`. Consumed -/// by [`lock`](LockKey::lock) and re-emitted at the new level. +/// by [`lock`](MutexKey::lock) and re-emitted at the new level. /// /// - `!Send + !Sync` — cannot cross thread boundaries. /// - Branded lifetime `'scope` — cannot escape the [`lock_scope`] closure. /// - `!Clone + !Copy` — prevents using the same key twice. -pub struct LockKey<'scope, Lvl: Level> { +pub struct MutexKey<'scope, Lvl: Level> { // Invariant in 'scope: fn(&'scope ()) -> &'scope () makes 'scope // neither covariant nor contravariant, preventing lifetime widening. _brand: PhantomData &'scope ()>, @@ -30,45 +32,109 @@ pub struct LockKey<'scope, Lvl: Level> { _level: PhantomData, } -impl core::fmt::Debug for LockKey<'_, Lvl> { +impl core::fmt::Debug for MutexKey<'_, Lvl> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("LockKey").finish_non_exhaustive() + f.debug_struct("MutexKey").finish_non_exhaustive() } } -impl<'scope, Lvl: Level> LockKey<'scope, Lvl> { - /// Acquire all locks in the set. +impl<'scope, Lvl: Level> MutexKey<'scope, Lvl> { + /// Create a new key. Only callable within the crate -- + /// external users get keys from [`lock_scope`], [`try_lock_scope`], + /// or [`KeyCell::grant`](crate::key_cell::KeyCell::grant). + pub(crate) fn new_internal() -> Self { + Self { + _brand: PhantomData, + _not_send: PhantomData, + _level: PhantomData, + } + } + + /// Lock one or more mutexes and pass the guards to a closure. + /// + /// Sorts by [`LockId`](crate::id::LockId) and acquires in one + /// call. The guards live inside the closure -- no [`LockSet`] + /// needed. + /// + /// For hot paths where the same locks are acquired repeatedly, + /// pre-build a [`LockSet`] and use [`acquire`](MutexKey::acquire) + /// instead (sort once, lock many). + /// + /// Consumes the key. Returns the closure's result plus a new key + /// at the lockable's level. Infallible -- if the level + /// relationship isn't declared, the code doesn't compile. + /// + /// # Examples + /// + /// ```rust,ignore + /// use surelock::{key::lock_scope, mutex::Mutex}; + /// + /// let a: Mutex = Mutex::new(1); + /// let b: Mutex = Mutex::new(2); + /// + /// lock_scope(|key| { + /// let (sum, _key) = key.lock((&a, &b), |(ga, gb)| *ga + *gb); + /// assert_eq!(sum, 3); + /// }); + /// ``` + #[allow(clippy::indexing_slicing)] + pub fn lock<'a, L, F, Ret>( + self, + lockable: &'a L, + f: F, + ) -> (Ret, MutexKey<'scope, >::Lvl>) + where + L: Lockable<'a>, + >::Lvl: LockAfter, + F: FnOnce(>::Guard) -> Ret, + { + // Build sorted index permutation inline. + let mut ids = alloc::vec::Vec::new(); + lockable.collect_ids(&mut ids); + let mut indices: alloc::vec::Vec = (0..ids.len()).collect(); + indices.sort_by_key(|&i| ids[i]); + + // SAFETY: indices is a valid permutation of 0..N built from + // collect_ids. The LockAfter bound guarantees level ordering. + let guards = unsafe { lockable.lock_sorted(&indices) }; + let result = f(guards); + + (result, MutexKey::new_internal()) + } + + /// Acquire all locks in a pre-built [`LockSet`], returning the + /// guards directly. /// - /// The set's level must implement [`LockAfter`] for the key's - /// current level. Consumes the key and returns guards plus a - /// new key at the set's level. + /// Use this for hot paths where the same set is locked + /// repeatedly (sort once, lock many times). For one-off + /// acquisitions, [`lock`](MutexKey::lock) is more convenient. /// - /// This method is infallible — if the level relationship isn't + /// Consumes the key and returns guards plus a new key at the + /// set's level. Infallible -- if the level relationship isn't /// declared, the code doesn't compile. - pub fn lock<'set, L>(self, set: &'set LockSet) -> (L::Guards<'set>, LockKey<'scope, L::Lvl>) + pub fn acquire<'set, L>( + self, + set: &'set LockSet, + ) -> ( + >::Guard, + MutexKey<'scope, >::Lvl>, + ) where - L: Lockable, - L::Lvl: LockAfter, + L: Lockable<'set>, + >::Lvl: LockAfter, { // SAFETY: LockSet guarantees sorted_indices is a valid // permutation built from collect_ids. The LockAfter bound // guarantees the level ordering is safe. let guards = unsafe { set.lock_sorted() }; - ( - guards, - LockKey { - _brand: PhantomData, - _not_send: PhantomData, - _level: PhantomData, - }, - ) + (guards, MutexKey::new_internal()) } /// Create a nested scope that inherits the current level. /// /// The inner key starts at the same level as the outer key, so - /// any subsequent [`lock`](LockKey::lock) calls must still be at + /// any subsequent [`lock`](MutexKey::lock) calls must still be at /// higher levels. The outer key is consumed for the duration of /// the subscope. /// @@ -76,11 +142,11 @@ impl<'scope, Lvl: Level> LockKey<'scope, Lvl> { /// alongside the closure's return value. The inner key gets its /// own branded lifetime, so its guards cannot escape the inner /// closure. - pub fn subscope(self, f: F) -> (Ret, LockKey<'scope, Lvl>) + pub fn subscope(self, f: F) -> (Ret, MutexKey<'scope, Lvl>) where - F: for<'inner> FnOnce(LockKey<'inner, Lvl>) -> Ret, + F: for<'inner> FnOnce(MutexKey<'inner, Lvl>) -> Ret, { - let result = f(LockKey { + let result = f(MutexKey { _brand: PhantomData, _not_send: PhantomData, _level: PhantomData, @@ -88,7 +154,7 @@ impl<'scope, Lvl: Level> LockKey<'scope, Lvl> { ( result, - LockKey { + MutexKey { _brand: PhantomData, _not_send: PhantomData, _level: PhantomData, @@ -97,27 +163,89 @@ impl<'scope, Lvl: Level> LockKey<'scope, Lvl> { } } -/// Entry point for all ordered lock acquisition. +/// Try to enter an ordered lock acquisition scope. +/// +/// Returns `Some(result)` if a scope was entered successfully, or +/// `None` if a scope is already active on the current thread (on +/// `std`). Use [`lock_scope`] for a panicking convenience wrapper. /// /// The `for<'scope>` bound makes `'scope` universally quantified: /// the closure must work for _any_ `'scope`, so `Ret` cannot name /// `'scope`. This prevents keys and guards from escaping the closure. /// Same technique as [`std::thread::scope`]. /// +/// # Scope Uniqueness +/// /// On `std` (with the `std` feature enabled), a `thread_local!` flag -/// prevents nested `lock_scope` calls on the same thread. On `no_std`, -/// nested calls are a documented hazard — use -/// [`LockKey::subscope`] for safe nesting. +/// detects nested calls and returns `None`. +/// +/// On `no_std`, **this check is not available** -- the function +/// always returns `Some`. Calling `try_lock_scope` while already +/// inside a scope on the same execution context creates two +/// independent [`MutexKey`]s with independent level tracking. This +/// defeats the ordering guarantee: +/// +/// ```text +/// // DANGEROUS on no_std -- two independent keys +/// try_lock_scope(|key1| { +/// try_lock_scope(|key2| { +/// // key1 and key2 don't know about each other. +/// // Thread A: key1 locks X, key2 locks Y +/// // Thread B: key1 locks Y, key2 locks X +/// // --> deadlock +/// }); +/// }); +/// ``` +/// +/// Use [`MutexKey::subscope`] instead -- it inherits the outer key's +/// level and is compile-time safe. +/// +/// # Examples +/// +/// ```rust,ignore +/// use surelock::{key::try_lock_scope, mutex::Mutex}; +/// +/// let counter: Mutex = Mutex::new(0); +/// +/// let result = try_lock_scope(|key| { +/// let set = LockSet::new(&counter); +/// let (mut guard, _key) = key.lock(&set); +/// *guard += 1; +/// }); +/// +/// match result { +/// Some(()) => { /* success */ } +/// None => { /* already inside a scope */ } +/// } +/// ``` +pub fn try_lock_scope(f: F) -> Option +where + F: for<'scope> FnOnce(MutexKey<'scope, Bottom>) -> Ret, +{ + let mut keeper = crate::key_cell::KeyCell::try_claim()?; + Some(keeper.grant(f)) +} + +/// Enter an ordered lock acquisition scope, or panic if one is +/// already active on this thread. +/// +/// Convenience wrapper around [`try_lock_scope`] that panics on +/// nested calls instead of returning `None`. Use this at top-level +/// call sites where nesting is a programming error. Use +/// [`try_lock_scope`] in library code that may be called from +/// either inside or outside a scope. +/// +/// For nested locking within a scope, use [`MutexKey::subscope`]. /// /// # Panics /// -/// On `std`: panics if a `lock_scope` is already active on the current -/// thread. Use [`LockKey::subscope`] for nesting instead. +/// On `std`: panics if a scope is already active on the current +/// thread. /// /// # Examples /// /// ```rust,ignore -/// use surelock::{key::lock_scope, mutex::Mutex, set::LockSet}; +/// use surelock::{key::lock_scope, mutex::Mutex}; /// /// let counter: Mutex = Mutex::new(0); /// @@ -127,45 +255,10 @@ impl<'scope, Lvl: Level> LockKey<'scope, Lvl> { /// *guard += 1; /// }); /// ``` +#[allow(clippy::expect_used)] // Intentional -- nested lock_scope is a programming error pub fn lock_scope(f: F) -> Ret where - F: for<'scope> FnOnce(LockKey<'scope, Bottom>) -> Ret, + F: for<'scope> FnOnce(MutexKey<'scope, Bottom>) -> Ret, { - #[cfg(feature = "std")] - { - extern crate std; - - std::thread_local! { - static SCOPE_ACTIVE: core::cell::Cell = - const { core::cell::Cell::new(false) }; - } - - SCOPE_ACTIVE.with(|active| { - assert!( - !active.replace(true), - "nested lock_scope -- use key.subscope() instead" - ); - }); - - let result = f(LockKey { - _brand: PhantomData, - _not_send: PhantomData, - _level: PhantomData, - }); - - SCOPE_ACTIVE.with(|active| { - active.set(false); - }); - - result - } - - #[cfg(not(feature = "std"))] - { - f(LockKey { - _brand: PhantomData, - _not_send: PhantomData, - _level: PhantomData, - }) - } + try_lock_scope(f).expect("nested lock_scope -- use key.subscope() or try_lock_scope()") } diff --git a/surelock/src/key_cell.rs b/surelock/src/key_cell.rs new file mode 100644 index 0000000..e2846a2 --- /dev/null +++ b/surelock/src/key_cell.rs @@ -0,0 +1,174 @@ +//! Per-thread capability for lock scope creation. +//! +//! A [`KeyCell`] is the root of authority for lock acquisition on +//! its thread. It is claimed once per thread via [`KeyCell::claim`] +//! or [`KeyCell::try_claim`], and provides [`grant`](KeyCell::grant) +//! for creating [`MutexKey`](crate::key::MutexKey)s inside a branded- +//! lifetime closure. +//! +//! `grant(&mut self)` takes a mutable borrow, so the borrow checker +//! prevents nesting at compile time -- no runtime check needed. This +//! is the static alternative to [`try_lock_scope`](crate::key::try_lock_scope) +//! / [`lock_scope`](crate::key::lock_scope), which use runtime checks. +//! +//! # Per-Thread, Not Global +//! +//! `KeyCell` is `!Send` and `!Sync`. Each thread claims its own +//! keeper. Cross-thread deadlock prevention comes from +//! [`LockSet`](crate::set::LockSet)'s sorted acquisition and +//! [`LockAfter`](crate::level::LockAfter)'s level ordering -- those +//! are properties of the locks, not the keeper. +//! +//! # Storage Model +//! +//! On `std`, each thread has a `KeyCell` pre-allocated in +//! `thread_local!` storage. [`try_claim`](KeyCell::try_claim) +//! takes it out (`.take()`), and [`Drop`] puts it back. This ensures +//! at most one active keeper per thread by construction -- no boolean +//! flag needed. +//! +//! On `no_std`, `try_claim` always succeeds (no `thread_local!`). +//! The `&mut self` on `grant` is the only nesting prevention. +//! +//! # Examples +//! +//! ```rust,ignore +//! use surelock::{key_cell::KeyCell, mutex::Mutex, set::LockSet}; +//! +//! let mut keeper = KeyCell::claim(); +//! +//! keeper.grant(|key| { +//! let m: Mutex = Mutex::new(42); +//! let set = surelock::set::LockSet::new(&m); +//! let (guard, _key) = key.lock(&set); +//! assert_eq!(*guard, 42); +//! }); +//! +//! // Sequential grants are fine +//! keeper.grant(|key| { +//! // fresh scope, new key at Bottom +//! }); +//! +//! // Nesting is a compile error: +//! // keeper.grant(|key1| { +//! // keeper.grant(|key2| { ... }); +//! // ^^^^ error: already mutably borrowed +//! // }); +//! ``` + +use core::marker::PhantomData; + +use crate::{key::MutexKey, level::Bottom}; + +/// Sentinel type stored in `thread_local!`. Its presence means +/// no keeper has been claimed on this thread. +#[cfg(feature = "std")] +struct KeyCellSlot; + +#[cfg(feature = "std")] +mod storage { + extern crate std; + + use core::cell::Cell; + + use super::KeyCellSlot; + + std::thread_local! { + static SLOT: Cell> = + const { Cell::new(Some(KeyCellSlot)) }; + } + + /// Take the keeper slot from thread-local storage. + /// Returns `Some` if available (first claim), `None` if already taken. + pub(crate) fn take() -> Option { + SLOT.with(Cell::take) + } + + /// Put the keeper slot back into thread-local storage. + pub(crate) fn put_back() { + SLOT.with(|cell| { + cell.set(Some(KeyCellSlot)); + }); + } +} + +/// Per-thread capability to create lock scopes. +/// +/// Claimed once per thread via [`try_claim`](KeyCell::try_claim). +/// [`grant`](KeyCell::grant) takes `&mut self`, so the borrow +/// checker prevents nesting at compile time. +/// +/// `!Clone`, `!Copy`, `!Send`, `!Sync`. +pub struct KeyCell { + _not_send: PhantomData<*const ()>, +} + +impl KeyCell { + /// Try to claim the `KeyCell` for the current thread. + /// + /// On `std`: takes the keeper from `thread_local!` storage. + /// Returns `None` if already claimed (by a previous `try_claim` + /// or an active `try_lock_scope` / `lock_scope`). + /// + /// On `no_std`: always returns `Some` (no `thread_local!`). + /// The `&mut self` on [`grant`](KeyCell::grant) is the only + /// nesting prevention. + #[must_use] + pub fn try_claim() -> Option { + #[cfg(feature = "std")] + { + storage::take().map(|_slot| Self { + _not_send: PhantomData, + }) + } + + #[cfg(not(feature = "std"))] + { + Some(Self { + _not_send: PhantomData, + }) + } + } + + /// Claim the `KeyCell` for the current thread, panicking if + /// already claimed. + /// + /// # Panics + /// + /// Panics if a `KeyCell` or active lock scope already exists + /// on this thread. + #[must_use] + #[allow(clippy::expect_used, clippy::new_without_default)] + pub fn claim() -> Self { + Self::try_claim().expect("surelock: KeyCell already claimed on this thread") + } + + /// Grant a [`MutexKey`] to the closure for ordered lock acquisition. + /// + /// Takes `&mut self` -- the borrow checker prevents calling + /// `grant` while already inside a `grant` (compile error, not + /// runtime check). This is the static nesting prevention + /// mechanism. + /// + /// The granted key starts at [`Bottom`] and advances through + /// levels as locks are acquired. + pub fn grant(&mut self, f: F) -> Ret + where + F: for<'scope> FnOnce(MutexKey<'scope, Bottom>) -> Ret, + { + f(MutexKey::new_internal()) + } +} + +impl Drop for KeyCell { + fn drop(&mut self) { + #[cfg(feature = "std")] + storage::put_back(); + } +} + +impl core::fmt::Debug for KeyCell { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("KeyCell").finish_non_exhaustive() + } +} diff --git a/surelock/src/level.rs b/surelock/src/level.rs index a42f62d..c99af4d 100644 --- a/surelock/src/level.rs +++ b/surelock/src/level.rs @@ -28,7 +28,7 @@ pub trait LockAfter: Level {} /// The bottom of the ordering lattice. /// -/// This is the initial state of [`super::LockKey`] when created by +/// This is the initial state of [`super::MutexKey`] when created by /// [`super::lock_scope`]. Not a valid level for mutexes — only used /// internally as the starting point for the key's level parameter. #[derive(Debug, Clone, Copy)] diff --git a/surelock/src/lib.rs b/surelock/src/lib.rs index ce1b641..8abf4d2 100644 --- a/surelock/src/lib.rs +++ b/surelock/src/lib.rs @@ -8,7 +8,7 @@ //! creation. Safe by construction, infallible. //! 2. **Levels** -- named types declaring a compile-time ordering via //! [`level::LockAfter`] trait bounds. A consumed-and-re-emitted -//! [`key::LockKey`] tracks the current level as a type parameter. +//! [`key::MutexKey`] tracks the current level as a type parameter. //! Wrong-order acquisition is a compile error, not a runtime failure. //! //! Every lock call is infallible or doesn't compile. No `Result`, @@ -38,6 +38,22 @@ //! [`lock_api::RawMutex`] backend directly. The level parameter //! defaults to [`level::Base`] -- levels are opt-in for incremental //! cross-level acquisition. +//! +//! # Scope Entry +//! +//! There are two ways to enter a lock scope: +//! +//! - **Ambient**: [`key::lock_scope`] / [`key::try_lock_scope`] -- +//! convenient, runtime nesting check on `std` (panic / `None`). +//! - **Capability-based**: [`key_cell::KeyCell::grant`] -- +//! static nesting prevention via `&mut self` (compile error). +//! Works on `no_std` without `thread_local!`. +//! +//! On `no_std`, the ambient entry points have no nesting check. +//! [`KeyCell`](key_cell::KeyCell) is the only way to get +//! static nesting prevention on `no_std`. See +//! [`key::MutexKey::subscope`] for safe nesting within an existing +//! scope on all targets. // Internal unsafe is required for RawMutex implementations and // MutexGuard's Deref/DerefMut. All unsafe is confined to this crate; @@ -49,6 +65,7 @@ extern crate alloc; pub mod id; pub mod key; +pub mod key_cell; pub mod level; pub mod lockable; pub mod mutex; diff --git a/surelock/src/lockable.rs b/surelock/src/lockable.rs index 87299d8..bfed68b 100644 --- a/surelock/src/lockable.rs +++ b/surelock/src/lockable.rs @@ -3,6 +3,11 @@ //! [`Lockable`] abstracts over single mutexes, tuples of mutexes, //! and slices of mutexes. All elements in a `Lockable` must share //! the same lock [`Level`](crate::level::Level). +//! +//! The lifetime parameter `'a` on the trait represents the borrow +//! of the lockable -- guards returned by +//! [`lock_sorted`](Lockable::lock_sorted) borrow from the mutexes +//! for this lifetime. See ADR-20. pub mod tuples; @@ -11,7 +16,7 @@ use alloc::vec::Vec; use crate::{ id::LockId, level::Level, - mutex::{Mutex, guard::MutexGuard}, + mutex::{guard::MutexGuard, Mutex}, raw_mutex::RawMutex, }; @@ -19,25 +24,19 @@ use crate::{ /// /// All locks in a single `Lockable` must be at the same level. /// -/// Implementors should: -/// - Correctly report their [`LockId`]s via -/// [`collect_ids`](Lockable::collect_ids). -/// - Lock the correct mutexes in -/// [`lock_sorted`](Lockable::lock_sorted) according to the -/// provided index permutation. -/// - Ensure all locks share the same level type `Lvl`. +/// The lifetime `'a` is the borrow lifetime of the lockable. Guards +/// returned by [`lock_sorted`](Lockable::lock_sorted) borrow from +/// the underlying mutexes for this lifetime. /// /// This is a safe trait because a wrong implementation can only /// cause deadlocks (a liveness failure), not undefined behaviour. /// The soundness invariant is on [`RawMutex`], not on `Lockable`. -pub trait Lockable { +pub trait Lockable<'a> { /// The lock level shared by all locks in this collection. type Lvl: Level; /// The guard type(s) returned when all locks are held. - type Guards<'set> - where - Self: 'set; + type Guard; /// Collect the [`LockId`] of each lock into `out`. fn collect_ids(&self, out: &mut Vec); @@ -45,30 +44,43 @@ pub trait Lockable { /// Acquire all locks in the order given by `sorted_indices`, /// returning the guards. /// - /// `sorted_indices` are indices into the conceptual array of - /// locks, ordered by their [`LockId`]. - /// /// # Safety /// /// `sorted_indices` must be a valid permutation produced by /// [`LockSet`](crate::set::LockSet). - unsafe fn lock_sorted(&self, sorted_indices: &[usize]) -> Self::Guards<'_>; + unsafe fn lock_sorted(&'a self, sorted_indices: &[usize]) -> Self::Guard; } // -- Single Mutex Implementation -- -impl Lockable for &Mutex { +// Owned mutex -- used by key.lock(&mutex, |guard| ...) +impl<'a, T: 'a, R: RawMutex + 'a, Lvl: Level> Lockable<'a> for Mutex { + type Lvl = Lvl; + type Guard = MutexGuard<'a, R, T>; + + fn collect_ids(&self, out: &mut Vec) { + out.push(self.id()); + } + + unsafe fn lock_sorted(&'a self, _sorted_indices: &[usize]) -> Self::Guard { + let raw_guard = self.raw.lock(); + MutexGuard { + data: &self.data, + _raw_guard: raw_guard, + } + } +} + +// Reference to mutex -- used by LockSet (which stores &Mutex) +impl<'a, T: 'a, R: RawMutex + 'a, Lvl: Level> Lockable<'a> for &'a Mutex { type Lvl = Lvl; - type Guards<'set> - = MutexGuard<'set, R, T> - where - Self: 'set; + type Guard = MutexGuard<'a, R, T>; fn collect_ids(&self, out: &mut Vec) { out.push(self.id()); } - unsafe fn lock_sorted(&self, _sorted_indices: &[usize]) -> Self::Guards<'_> { + unsafe fn lock_sorted(&'a self, _sorted_indices: &[usize]) -> Self::Guard { let raw_guard = self.raw.lock(); MutexGuard { data: &self.data, diff --git a/surelock/src/lockable/tuples.rs b/surelock/src/lockable/tuples.rs index a5116f2..98d7d74 100644 --- a/surelock/src/lockable/tuples.rs +++ b/surelock/src/lockable/tuples.rs @@ -6,14 +6,11 @@ use crate::{ lockable::Lockable, - mutex::{Mutex, guard::MutexGuard}, + mutex::{guard::MutexGuard, Mutex}, raw_mutex::RawMutex, }; /// Lock a single element by index, returning an Option-wrapped guard. -/// Returns `Some` if `target` matches `idx`, `None` otherwise. -/// This helper allows the tuple macro to lock elements one at a time -/// in sorted order without needing `MaybeUninit` or tuple-of-GATs. macro_rules! lock_if_match { ($self:expr, $target:expr, $idx:tt) => { if $target == $idx { @@ -29,25 +26,19 @@ macro_rules! lock_if_match { macro_rules! impl_lockable_tuple { ($n:literal: $($idx:tt $T:ident),+) => { - impl<$($T,)+ R: RawMutex, Lvl: crate::level::Level> Lockable - for ($(&Mutex<$T, R, Lvl>,)+) + impl<'a, $($T,)+ R: RawMutex, Lvl: crate::level::Level> Lockable<'a> + for ($(&'a Mutex<$T, R, Lvl>,)+) { type Lvl = Lvl; - type Guards<'set> - = ($(MutexGuard<'set, R, $T>,)+) - where - Self: 'set; + type Guard = ($(MutexGuard<'a, R, $T>,)+); fn collect_ids(&self, out: &mut alloc::vec::Vec) { $(out.push(self.$idx.id());)+ } #[allow(clippy::indexing_slicing, non_snake_case)] - unsafe fn lock_sorted(&self, sorted_indices: &[usize]) -> Self::Guards<'_> { - // Lock in sorted order, collecting each guard into its - // positional Option slot. After all indices are processed, - // every slot is Some. - $(let mut $T: Option> = None;)+ + unsafe fn lock_sorted(&'a self, sorted_indices: &[usize]) -> Self::Guard { + $(let mut $T: Option> = None;)+ for &target in sorted_indices { $( @@ -59,8 +50,6 @@ macro_rules! impl_lockable_tuple { unreachable!(); } - // SAFETY: sorted_indices is a permutation of 0..N, - // so every slot has been filled. ($( $T.expect("lock_sorted: missing index in permutation"), )+) diff --git a/surelock/src/mutex.rs b/surelock/src/mutex.rs index 35c30dc..584d484 100644 --- a/surelock/src/mutex.rs +++ b/surelock/src/mutex.rs @@ -3,7 +3,7 @@ //! [`Mutex`] wraps a [`RawMutex`](crate::raw_mutex::RawMutex) //! implementation and a `T`, tagging the pair with a lock [`Level`]. //! All ordered locking goes through -//! [`LockKey`](super::key::LockKey) + +//! [`MutexKey`](super::key::MutexKey) + //! [`LockSet`](super::set::LockSet) -- there is no public `lock()` //! method by default. //! @@ -33,13 +33,13 @@ use guard::MutexGuard; /// levels are opt-in for incremental cross-level acquisition. /// /// All ordered locking goes through -/// [`LockKey::lock`](super::key::LockKey::lock). There is no public +/// [`MutexKey::lock`](super::key::MutexKey::lock). There is no public /// `lock()` method unless the `escape-hatch` feature is enabled. /// /// # Examples /// /// ```rust,ignore -/// use surelock::{key::lock_scope, mutex::Mutex, set::LockSet}; +/// use surelock::{key::lock_scope, mutex::Mutex}; /// /// let counter: Mutex = Mutex::new(0); /// @@ -71,7 +71,7 @@ pub struct Mutex = Mutex>; /// let counter: M = Mutex::new(0); diff --git a/surelock/src/mutex/guard.rs b/surelock/src/mutex/guard.rs index 8c23e92..4949ac2 100644 --- a/surelock/src/mutex/guard.rs +++ b/surelock/src/mutex/guard.rs @@ -17,7 +17,7 @@ use crate::raw_mutex::RawMutex; /// The lock is released when the guard is dropped. /// /// Guards are not constructible by users -- they are returned by -/// [`LockKey::lock`](crate::key::LockKey::lock) (via +/// [`MutexKey::lock`](crate::key::MutexKey::lock) (via /// [`LockSet`](crate::set::LockSet)) or /// [`Mutex::unchecked_lock`](super::Mutex::unchecked_lock) /// (if the `escape-hatch` feature is enabled). diff --git a/surelock/src/set.rs b/surelock/src/set.rs index e515a53..317791d 100644 --- a/surelock/src/set.rs +++ b/surelock/src/set.rs @@ -2,16 +2,16 @@ //! //! A [`LockSet`] wraps a [`Lockable`] and pre-computes the acquisition //! order (sorted by [`LockId`]). Construct once, lock many times via -//! [`LockKey::lock`](crate::key::LockKey::lock). +//! [`MutexKey::acquire`](crate::key::MutexKey::acquire). use alloc::vec::Vec; use crate::lockable::Lockable; -/// A prepared set of locks, pre-sorted by [`LockId`](crate::LockId). +/// A prepared set of locks, pre-sorted by [`LockId`](crate::id::LockId). /// /// All locks in a `LockSet` must be at the same level. Construct once, -/// lock many times via [`LockKey::lock`](crate::key::LockKey::lock). +/// lock many times via [`MutexKey::acquire`](crate::key::MutexKey::acquire). /// /// # Examples /// @@ -24,13 +24,13 @@ use crate::lockable::Lockable; /// // Sort happens once at construction time. /// let set = LockSet::new((&a, &b)); /// ``` -pub struct LockSet { +pub struct LockSet { pub(crate) lockable: L, /// Indices into the lockable, sorted by `LockId`. pub(crate) sorted_indices: Vec, } -impl core::fmt::Debug for LockSet { +impl core::fmt::Debug for LockSet { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("LockSet") .field("sorted_indices", &self.sorted_indices) @@ -38,14 +38,17 @@ impl core::fmt::Debug for LockSet { } } -impl LockSet { +impl LockSet { /// Create a new `LockSet`, pre-sorting by [`LockId`](crate::id::LockId). /// /// # Panics /// /// Debug-asserts that no duplicate locks are present. #[allow(clippy::indexing_slicing)] // indices are 0..ids.len(), always in bounds - pub fn new(lockable: L) -> Self { + pub fn new<'a>(lockable: L) -> Self + where + L: Lockable<'a>, + { let mut ids = Vec::new(); lockable.collect_ids(&mut ids); @@ -70,7 +73,10 @@ impl LockSet { /// /// Caller must ensure this is called at most once before the /// returned guards are dropped (i.e., no double-locking). - pub(crate) unsafe fn lock_sorted(&self) -> L::Guards<'_> { + pub(crate) unsafe fn lock_sorted<'a>(&'a self) -> >::Guard + where + L: Lockable<'a>, + { unsafe { self.lockable.lock_sorted(&self.sorted_indices) } } } diff --git a/surelock/tests/integration.rs b/surelock/tests/integration.rs index 6d38459..8e65ef3 100644 --- a/surelock/tests/integration.rs +++ b/surelock/tests/integration.rs @@ -2,44 +2,80 @@ //! //! Uses the default `StdMutex` backend (std feature, enabled by default). -use surelock::{key::lock_scope, lock_order, mutex::Mutex, set::LockSet}; +use surelock::{ + key::{lock_scope, try_lock_scope}, + key_cell::KeyCell, + lock_order, + mutex::Mutex, + set::LockSet, +}; -// -- Single LockSet (no levels needed) -- +// -- Convenience .lock() (closure-based, one-shot) -- #[test] -fn single_lock() { +fn lock_single() { let mut counter: Mutex = Mutex::new(0); lock_scope(|key| { - let set = LockSet::new(&counter); - let (mut guard, _key) = key.lock(&set); - *guard += 1; + let ((), _key) = key.lock(&counter, |mut guard| { + *guard += 1; + }); }); assert_eq!(*counter.get_mut(), 1); } #[test] -fn single_lock_returned_value() { +fn lock_returns_value() { let counter: Mutex = Mutex::new(42); let value = lock_scope(|key| { - let set = LockSet::new(&counter); - let (guard, _key) = key.lock(&set); - *guard + let (v, _key) = key.lock(&counter, |guard| *guard); + v }); assert_eq!(value, 42); } #[test] -fn two_locks_atomic() { +fn lock_two_atomic() { + let mut a: Mutex = Mutex::new(10); + let mut b: Mutex = Mutex::new(20); + + lock_scope(|key| { + let ((), _key) = key.lock(&(&a, &b), |(mut ga, mut gb)| { + *ga += 1; + *gb += 1; + }); + }); + + assert_eq!(*a.get_mut(), 11); + assert_eq!(*b.get_mut(), 21); +} + +// -- Pre-built LockSet .acquire() (guards returned) -- + +#[test] +fn acquire_single() { + let mut counter: Mutex = Mutex::new(0); + + let set = LockSet::new(&counter); + lock_scope(|key| { + let (mut guard, _key) = key.acquire(&set); + *guard += 1; + }); + + assert_eq!(*counter.get_mut(), 1); +} + +#[test] +fn acquire_two_atomic() { let mut a: Mutex = Mutex::new(10); let mut b: Mutex = Mutex::new(20); + let set = LockSet::new((&a, &b)); lock_scope(|key| { - let set = LockSet::new((&a, &b)); - let ((mut ga, mut gb), _key) = key.lock(&set); + let ((mut ga, mut gb), _key) = key.acquire(&set); *ga += 1; *gb += 1; }); @@ -59,23 +95,53 @@ type LowMutex = Mutex; type HighMutex = Mutex; #[test] -fn cross_level_acquisition() { +fn cross_level_with_lock() { let config: LowMutex = Mutex::new(5); let mut account: HighMutex = Mutex::new(100); lock_scope(|key| { - let config_set = LockSet::new(&config); - let (cfg, key) = key.lock(&config_set); + let (bonus, key) = key.lock(&config, |cfg| *cfg); + let ((), _key) = key.lock(&account, |mut acct| { + *acct += bonus; + }); + }); + + assert_eq!(*account.get_mut(), 105); +} - let acct_set = LockSet::new(&account); - let (mut acct, _key) = key.lock(&acct_set); +#[test] +fn cross_level_with_acquire() { + let config: LowMutex = Mutex::new(5); + let mut account: HighMutex = Mutex::new(100); + let config_set = LockSet::new(&config); + let acct_set = LockSet::new(&account); + + lock_scope(|key| { + let (cfg, key) = key.acquire(&config_set); + let (mut acct, _key) = key.acquire(&acct_set); *acct += *cfg; }); assert_eq!(*account.get_mut(), 105); } +#[test] +fn cross_level_mixed() { + let config: LowMutex = Mutex::new(5); + let mut account: HighMutex = Mutex::new(100); + + let acct_set = LockSet::new(&account); + + lock_scope(|key| { + let (bonus, key) = key.lock(&config, |cfg| *cfg); + let (mut acct, _key) = key.acquire(&acct_set); + *acct += bonus; + }); + + assert_eq!(*account.get_mut(), 105); +} + // -- Subscope -- #[test] @@ -84,15 +150,14 @@ fn subscope_inherits_level() { let mut account: HighMutex = Mutex::new(0); lock_scope(|key| { - let config_set = LockSet::new(&config); - let (cfg, key) = key.lock(&config_set); - let val = *cfg; + let (val, key) = key.lock(&config, |cfg| *cfg); let (result, _key) = key.subscope(|inner_key| { - let acct_set = LockSet::new(&account); - let (mut acct, _inner_key) = inner_key.lock(&acct_set); - *acct = val; - *acct + let (r, _inner_key) = inner_key.lock(&account, |mut acct| { + *acct = val; + *acct + }); + r }); assert_eq!(result, 42); @@ -101,6 +166,103 @@ fn subscope_inherits_level() { assert_eq!(*account.get_mut(), 42); } +// -- try_lock_scope -- + +#[test] +fn try_lock_scope_returns_some_when_no_scope_active() { + let counter: Mutex = Mutex::new(0); + + let result = try_lock_scope(|key| { + let (v, _key) = key.lock(&counter, |guard| *guard); + v + }); + + assert_eq!(result, Some(0)); +} + +#[test] +fn try_lock_scope_returns_none_when_nested() { + let result = lock_scope(|_key| try_lock_scope(|_key2| 42)); + assert_eq!(result, None); +} + +#[test] +fn try_lock_scope_recovers_after_inner_returns_none() { + lock_scope(|_key| { + let inner = try_lock_scope(|_key2| 42); + assert_eq!(inner, None); + }); + + let result = try_lock_scope(|_key| 99); + assert_eq!(result, Some(99)); +} + +// -- KeyCell -- + +#[test] +fn key_cell_grant_works() { + let mut keeper = KeyCell::claim(); + let mut counter: Mutex = Mutex::new(0); + + keeper.grant(|key| { + let ((), _key) = key.lock(&counter, |mut guard| { + *guard += 1; + }); + }); + + assert_eq!(*counter.get_mut(), 1); +} + +#[test] +fn key_cell_sequential_grants() { + let mut keeper = KeyCell::claim(); + + let a = keeper.grant(|_key| 1); + let b = keeper.grant(|_key| 2); + let c = keeper.grant(|_key| 3); + + assert_eq!(a + b + c, 6); +} + +#[test] +fn key_cell_prevents_second_claim() { + let _keeper = KeyCell::claim(); + assert!(KeyCell::try_claim().is_none()); +} + +#[test] +fn key_cell_blocks_try_lock_scope() { + let _keeper = KeyCell::claim(); + let result = try_lock_scope(|_key| 42); + assert_eq!(result, None); +} + +#[test] +fn key_cell_recovered_after_drop() { + { + let _keeper = KeyCell::claim(); + } + + let result = try_lock_scope(|_key| 99); + assert_eq!(result, Some(99)); +} + +#[test] +fn key_cell_with_levels() { + let mut keeper = KeyCell::claim(); + let config: LowMutex = Mutex::new(5); + let mut account: HighMutex = Mutex::new(100); + + keeper.grant(|key| { + let (bonus, key) = key.lock(&config, |cfg| *cfg); + let ((), _key) = key.lock(&account, |mut acct| { + *acct += bonus; + }); + }); + + assert_eq!(*account.get_mut(), 105); +} + // -- Mutable access without locking -- #[test] diff --git a/surelock_macros/src/lib.rs b/surelock_macros/src/lib.rs index fb2775e..db08c28 100644 --- a/surelock_macros/src/lib.rs +++ b/surelock_macros/src/lib.rs @@ -25,9 +25,9 @@ use proc_macro::TokenStream; use proc_macro2::Span; use quote::quote; use syn::{ - Ident, Token, parse::{Parse, ParseStream}, punctuated::Punctuated, + Ident, Token, }; /// A chain of identifiers separated by `=>`. @@ -116,7 +116,7 @@ pub fn lock_order(input: TokenStream) -> TokenStream { }); // Generate impl LockAfter and impl LockAfter for each type. - // Bottom is needed because the initial LockKey starts at Bottom. + // Bottom is needed because the initial MutexKey starts at Bottom. // Base is needed because Base-level locks can precede user-declared levels. let bottom_impls = levels.iter().map(|ident| { quote! {