You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@asyncband.apache.org by ti...@apache.org on 2026/08/26 06:44:23 UTC

(asyncband) 01/01: refactor(lazy-cell): make future storage explicit

This is an automated email from the ASF dual-hosted git repository.

tisonkun pushed a commit to branch codex/lazy-cell-inline-future
in repository https://gitbox.apache.org/repos/asf/asyncband.git

commit 019b4bbaec08f801ea9ecaa0f7f27250f68271fa
Author: tison <wa...@gmail.com>
AuthorDate: Wed Aug 26 14:41:38 2026 +0800

    refactor(lazy-cell): make future storage explicit
---
 asyncband/src/once/lazy_cell/mod.rs       | 284 +++++++++++++++++++-----------
 examples/Cargo.toml                       |   4 +
 examples/src/lazy_cell_boxed_vs_inline.rs |  61 +++++++
 examples/src/once_cell_vs_lazy_cell.rs    |  11 +-
 tests-integration/tests/lazy_cell_test.rs | 163 +++++++++++------
 tests-integration/tests/traits_test.rs    |   4 +-
 6 files changed, 366 insertions(+), 161 deletions(-)

diff --git a/asyncband/src/once/lazy_cell/mod.rs b/asyncband/src/once/lazy_cell/mod.rs
index a815555..efe134a 100644
--- a/asyncband/src/once/lazy_cell/mod.rs
+++ b/asyncband/src/once/lazy_cell/mod.rs
@@ -17,6 +17,8 @@
 
 use std::fmt;
 use std::future::Future;
+use std::future::Pending;
+use std::future::Ready;
 use std::panic::RefUnwindSafe;
 use std::panic::UnwindSafe;
 use std::pin::Pin;
@@ -26,17 +28,20 @@ use std::sync::atomic::Ordering;
 use crate::internal::value_cell::ValueCell;
 use crate::mutex::Mutex;
 
-type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
-
 /// A thread-safe value initialized by a stored asynchronous function on first access.
 ///
-/// Initialization starts when [`force`](Self::force) is polled. Concurrent callers wait without
-/// blocking their threads. If the forcing caller is cancelled, the initialization future remains
-/// pinned in the cell and the next caller resumes that same future instead of starting over.
+/// `LazyCell` stores the initializer's concrete future type without allocating or erasing it. The
+/// caller chooses whether that future lives inline in the cell or behind a pointer such as
+/// `Pin<Box<_>>`.
+///
+/// An `Unpin` future can be initialized with [`force`](Self::force). Otherwise the cell itself must
+/// be pinned and initialized with [`force_pin`](Self::force_pin). If the forcing caller is
+/// cancelled, the initialization future remains in the cell and the next caller resumes that same
+/// future instead of starting over.
 ///
-/// The initialization future must be `Send + 'static`: another task may resume it after the
-/// original caller is gone, potentially on another thread. The initializer itself only needs to
-/// be `Send`, not `Sync`, because the cell serializes access to it.
+/// The cell's `Send` and `Sync` implementations follow its value, initializer, and future. A local
+/// future may therefore borrow local data or be non-`Send`, while a cell shared between threads
+/// naturally requires those stored types to be `Send`.
 ///
 /// `LazyCell` represents one asynchronous initialization attempt. If initialization needs
 /// access-time arguments or should retry after returning an error, use `OnceCell::get_or_try_init`
@@ -45,8 +50,7 @@ type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
 /// # Poisoning
 ///
 /// A panic while creating or polling the initialization future permanently poisons the cell. The
-/// panic is propagated to its caller, and future calls to [`force`](Self::force) or
-/// [`force_mut`](Self::force_mut) panic.
+/// panic is propagated to its caller, and future calls to any forcing method panic.
 ///
 /// # Examples
 ///
@@ -55,63 +59,30 @@ type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
 /// # async fn main() {
 /// use asyncband::once::LazyCell;
 ///
-/// let lazy = LazyCell::<String, _>::new(async || "ready".to_owned());
+/// let lazy = LazyCell::new(async || "ready".to_owned());
+/// let lazy = std::pin::pin!(lazy);
 ///
 /// assert_eq!(LazyCell::get(&lazy), None);
-/// assert_eq!(LazyCell::force(&lazy).await, "ready");
+/// assert_eq!(LazyCell::force_pin(lazy.as_ref()).await, "ready");
 /// assert_eq!(LazyCell::get(&lazy).map(String::as_str), Some("ready"));
 /// # }
 /// ```
-pub struct LazyCell<T, F = fn() -> BoxFuture<T>> {
+pub struct LazyCell<T, Fut, F = fn() -> Fut> {
     value: ValueCell<T>,
-    state: Mutex<State<T, F>>,
+    state: Mutex<State<F, Fut>>,
     poisoned: AtomicBool,
 }
 
-struct State<T, F> {
+struct State<F, Fut> {
     initializer: Option<F>,
-    attempt: Option<BoxFuture<T>>,
-}
-
-impl<T, F> State<T, F> {
-    async fn drive_attempt<Fut>(&mut self, poisoned: &AtomicBool) -> T
-    where
-        F: FnOnce() -> Fut,
-        Fut: Future<Output = T> + Send + 'static,
-    {
-        if self.attempt.is_none() {
-            let initializer = self
-                .initializer
-                .take()
-                .expect("LazyCell initializer missing while uninitialized");
-            let future = {
-                let _poison = PoisonOnPanic(poisoned);
-                initializer()
-            };
-            self.attempt = Some(Box::pin(future));
-        }
-
-        let value = std::future::poll_fn(|cx| {
-            let _poison = PoisonOnPanic(poisoned);
-            self.attempt
-                .as_mut()
-                .expect("LazyCell attempt missing while initializing")
-                .as_mut()
-                .poll(cx)
-        })
-        .await;
-
-        // Treat panics from dropping a completed future as initializer panics as well.
-        let _poison = PoisonOnPanic(poisoned);
-        self.attempt = None;
-        value
-    }
+    attempt: Option<Fut>,
 }
 
-impl<T, F> LazyCell<T, F> {
+impl<T, Fut, F> LazyCell<T, Fut, F> {
     /// Creates a new lazy value with the given asynchronous initializer.
     ///
-    /// The initializer is not called until the first [`force`](Self::force) future is polled.
+    /// The initializer is not called until the first forcing future is polled. Its returned future
+    /// is stored as-is; this method never boxes it.
     ///
     /// # Examples
     ///
@@ -120,11 +91,28 @@ impl<T, F> LazyCell<T, F> {
     /// # async fn main() {
     /// use asyncband::once::LazyCell;
     ///
-    /// let lazy = LazyCell::<u32, _>::new(async || 92);
+    /// let lazy = LazyCell::new(async || 92);
+    /// let lazy = std::pin::pin!(lazy);
+    /// assert_eq!(*LazyCell::force_pin(lazy.as_ref()).await, 92);
+    /// # }
+    /// ```
+    ///
+    /// Or explicitly box the future so the cell remains movable:
+    ///
+    /// ```
+    /// # #[tokio::main]
+    /// # async fn main() {
+    /// use asyncband::once::LazyCell;
+    ///
+    /// let lazy = LazyCell::new(|| Box::pin(async { 92 }));
     /// assert_eq!(*LazyCell::force(&lazy).await, 92);
     /// # }
     /// ```
-    pub const fn new(initializer: F) -> Self {
+    pub const fn new(initializer: F) -> Self
+    where
+        F: FnOnce() -> Fut,
+        Fut: Future<Output = T>,
+    {
         Self {
             value: ValueCell::new(),
             state: Mutex::new(State {
@@ -151,48 +139,90 @@ impl<T, F> LazyCell<T, F> {
         this.value.get_mut()
     }
 
+    fn assert_unpoisoned(&self) {
+        if self.poisoned.load(Ordering::Acquire) {
+            panic_poisoned();
+        }
+    }
+}
+
+impl<T, Fut, F> LazyCell<T, Fut, F>
+where
+    F: FnOnce() -> Fut,
+    Fut: Future<Output = T> + Unpin,
+{
     /// Initializes the value if needed and returns a reference to it.
     ///
+    /// This method is available when moving the stored future is safe. A future behind
+    /// `Pin<Box<_>>` is `Unpin` because moving the box does not move its pointee.
+    ///
     /// If another task is initializing the cell, this call waits for that attempt. If the task
-    /// driving initialization is cancelled, a later caller resumes the same pinned future.
+    /// driving initialization is cancelled, a later caller resumes the same future.
     ///
     /// # Panics
     ///
     /// Panics if the initializer panics or the cell was previously poisoned. Recursive
     /// initialization of the same cell deadlocks.
-    pub async fn force<Fut>(this: &Self) -> &T
-    where
-        F: FnOnce() -> Fut,
-        Fut: Future<Output = T> + Send + 'static,
-    {
-        if let Some(value) = this.value.get() {
+    pub async fn force(this: &Self) -> &T {
+        Self::force_pin(Pin::new(this)).await
+    }
+
+    /// Initializes the value if needed and returns mutable access to it.
+    ///
+    /// # Panics
+    ///
+    /// Panics under the same conditions as [`force`](Self::force).
+    pub async fn force_mut(this: &mut Self) -> &mut T {
+        Self::force_pin_mut(Pin::new(this)).await
+    }
+}
+
+impl<T, Fut, F> LazyCell<T, Fut, F>
+where
+    F: FnOnce() -> Fut,
+    Fut: Future<Output = T>,
+{
+    /// Initializes a pinned cell and returns a reference to its value.
+    ///
+    /// Pinning the cell keeps an inline initialization future at a stable address. Cancellation
+    /// leaves that future in the cell so a later caller can resume it.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the initializer panics or the cell was previously poisoned. Recursive
+    /// initialization of the same cell deadlocks.
+    pub async fn force_pin<'a>(this: Pin<&'a Self>) -> &'a T {
+        let this_ref: &'a Self = Pin::get_ref(this);
+        if let Some(value) = Self::get(this_ref) {
             return value;
         }
-        this.assert_unpoisoned();
+        this_ref.assert_unpoisoned();
 
-        let mut state = this.state.lock().await;
-        if let Some(value) = this.value.get() {
+        let mut state = this_ref.state.lock().await;
+        if let Some(value) = Self::get(this_ref) {
             return value;
         }
-        this.assert_unpoisoned();
+        this_ref.assert_unpoisoned();
 
-        let value = state.drive_attempt(&this.poisoned).await;
+        // SAFETY: `this` remains pinned for the duration of the returned future. The state is
+        // stored in that cell, and a mutex guard never relocates its protected value.
+        let state = unsafe { Pin::new_unchecked(&mut *state) };
+        let value = drive_pinned_attempt(state, &this_ref.poisoned).await;
 
         // SAFETY: The state mutex serializes initialization, and the double check above verified
         // that the value was not initialized by another caller.
-        unsafe { this.value.set(value) }
+        unsafe { this_ref.value.set(value) }
     }
 
-    /// Initializes the value if needed and returns mutable access to it.
+    /// Initializes a pinned cell and returns mutable access to its value.
     ///
     /// # Panics
     ///
-    /// Panics under the same conditions as [`force`](Self::force).
-    pub async fn force_mut<Fut>(this: &mut Self) -> &mut T
-    where
-        F: FnOnce() -> Fut,
-        Fut: Future<Output = T> + Send + 'static,
-    {
+    /// Panics under the same conditions as [`force_pin`](Self::force_pin).
+    pub async fn force_pin_mut<'a>(this: Pin<&'a mut Self>) -> &'a mut T {
+        // SAFETY: We do not move the structurally pinned future through this reference. The stored
+        // value is not structurally pinned and may be accessed normally.
+        let this: &'a mut Self = unsafe { Pin::get_unchecked_mut(this) };
         if this.value.is_initialized_mut() {
             return this
                 .value
@@ -203,37 +233,91 @@ impl<T, F> LazyCell<T, F> {
             panic_poisoned();
         }
 
-        // Exclusive access makes locking and atomic value publication unnecessary.
-        let value = this.state.get_mut().drive_attempt(&this.poisoned).await;
+        // SAFETY: Exclusive access to the pinned cell pins its state in place for this call.
+        let state = unsafe { Pin::new_unchecked(this.state.get_mut()) };
+        let value = drive_pinned_attempt(state, &this.poisoned).await;
         this.value.set_mut(value)
     }
+}
 
-    fn assert_unpoisoned(&self) {
-        if self.poisoned.load(Ordering::Acquire) {
-            panic_poisoned();
+async fn drive_pinned_attempt<T, F, Fut>(
+    mut state: Pin<&mut State<F, Fut>>,
+    poisoned: &AtomicBool,
+) -> T
+where
+    F: FnOnce() -> Fut,
+    Fut: Future<Output = T>,
+{
+    if state.as_ref().get_ref().attempt.is_none() {
+        // SAFETY: The initializer is not structurally pinned and the attempt is still empty. The
+        // newly created future is written directly into its final pinned location.
+        let state = unsafe { state.as_mut().get_unchecked_mut() };
+        let initializer = state
+            .initializer
+            .take()
+            .expect("LazyCell initializer missing while uninitialized");
+        let future = {
+            let _poison = PoisonOnPanic(poisoned);
+            initializer()
+        };
+        state.attempt = Some(future);
+    }
+
+    let value = std::future::poll_fn(|cx| {
+        let _poison = PoisonOnPanic(poisoned);
+        // SAFETY: The state remains pinned while this future is polled, and the attempt is never
+        // moved after it is installed.
+        let state = unsafe { state.as_mut().get_unchecked_mut() };
+        let attempt = state
+            .attempt
+            .as_mut()
+            .expect("LazyCell attempt missing while initializing");
+        unsafe { Pin::new_unchecked(attempt) }.poll(cx)
+    })
+    .await;
+
+    // Assignment drops the completed future in place before clearing the slot. Treat a panic from
+    // that destructor as an initializer panic as well.
+    let _poison = PoisonOnPanic(poisoned);
+    // SAFETY: Dropping a pinned value in place is permitted, and no value is moved out.
+    unsafe { state.as_mut().get_unchecked_mut() }.attempt = None;
+    value
+}
+
+impl<T> Default for LazyCell<T, Ready<T>>
+where
+    T: Default,
+{
+    fn default() -> Self {
+        fn initialize<T: Default>() -> Ready<T> {
+            std::future::ready(T::default())
         }
+
+        Self::new(initialize::<T>)
     }
 }
 
-impl<T> LazyCell<T> {
+impl<T, Fut> LazyCell<T, Fut>
+where
+    Fut: Future<Output = T>,
+{
     /// Creates a new `LazyCell` from an already-created asynchronous future.
     ///
-    /// The future is stored without being polled. Calling [`force`](Self::force) starts polling
-    /// it, and cancellation preserves the in-flight future for a later caller.
-    pub fn from_future<Fut>(future: Fut) -> Self
-    where
-        Fut: Future<Output = T> + Send + 'static,
-    {
+    /// The future is stored as-is without being polled, boxed, or erased. Pin the returned cell
+    /// before forcing it when `Fut` is not `Unpin`.
+    pub fn from_future(future: Fut) -> Self {
         Self {
             value: ValueCell::new(),
             state: Mutex::new(State {
                 initializer: None,
-                attempt: Some(Box::pin(future)),
+                attempt: Some(future),
             }),
             poisoned: AtomicBool::new(false),
         }
     }
+}
 
+impl<T> LazyCell<T, Pending<T>> {
     /// Creates a new `LazyCell` that already contains `value`.
     pub const fn from_value(value: T) -> Self {
         Self {
@@ -247,20 +331,7 @@ impl<T> LazyCell<T> {
     }
 }
 
-impl<T> Default for LazyCell<T>
-where
-    T: Default,
-{
-    fn default() -> Self {
-        fn initialize<T: Default>() -> BoxFuture<T> {
-            Box::pin(async { T::default() })
-        }
-
-        Self::new(initialize::<T>)
-    }
-}
-
-impl<T: fmt::Debug, F> fmt::Debug for LazyCell<T, F> {
+impl<T: fmt::Debug, Fut, F> fmt::Debug for LazyCell<T, Fut, F> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         let mut tuple = f.debug_tuple("LazyCell");
         match Self::get(self) {
@@ -271,9 +342,14 @@ impl<T: fmt::Debug, F> fmt::Debug for LazyCell<T, F> {
     }
 }
 
-impl<T: UnwindSafe, F: UnwindSafe> UnwindSafe for LazyCell<T, F> {}
+impl<T, Fut: Unpin, F> Unpin for LazyCell<T, Fut, F> {}
 
-impl<T: RefUnwindSafe + UnwindSafe, F: UnwindSafe> RefUnwindSafe for LazyCell<T, F> {}
+impl<T: UnwindSafe, Fut: UnwindSafe, F: UnwindSafe> UnwindSafe for LazyCell<T, Fut, F> {}
+
+impl<T: RefUnwindSafe + UnwindSafe, Fut: UnwindSafe, F: UnwindSafe> RefUnwindSafe
+    for LazyCell<T, Fut, F>
+{
+}
 
 struct PoisonOnPanic<'a>(&'a AtomicBool);
 
diff --git a/examples/Cargo.toml b/examples/Cargo.toml
index c0bf64f..09b216f 100644
--- a/examples/Cargo.toml
+++ b/examples/Cargo.toml
@@ -35,3 +35,7 @@ workspace = true
 [[example]]
 name = "once_cell_vs_lazy_cell"
 path = "src/once_cell_vs_lazy_cell.rs"
+
+[[example]]
+name = "lazy_cell_boxed_vs_inline"
+path = "src/lazy_cell_boxed_vs_inline.rs"
diff --git a/examples/src/lazy_cell_boxed_vs_inline.rs b/examples/src/lazy_cell_boxed_vs_inline.rs
new file mode 100644
index 0000000..8feb15e
--- /dev/null
+++ b/examples/src/lazy_cell_boxed_vs_inline.rs
@@ -0,0 +1,61 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::sync::Arc;
+
+use asyncband::once::LazyCell;
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() {
+    // The library stores exactly the future returned by the initializer. Explicitly boxing that
+    // future keeps the cell movable, without requiring dynamic dispatch.
+    let boxed = LazyCell::new(|| Box::pin(async { "boxed".to_owned() }));
+    assert_eq!(LazyCell::force(&boxed).await, "boxed");
+
+    // Returning the async future directly stores it inline. The caller pins the cell before the
+    // first poll, so the future stays at a stable address without a heap allocation.
+    let inline = LazyCell::new(async || "inline".to_owned());
+    let mut inline = std::pin::pin!(inline);
+    assert_eq!(LazyCell::force_pin(inline.as_ref()).await, "inline");
+
+    LazyCell::force_pin_mut(inline.as_mut())
+        .await
+        .push_str(" future");
+    assert_eq!(
+        LazyCell::get(&inline).expect("value is initialized"),
+        "inline future"
+    );
+
+    // Arc::pin allocates the cell once and keeps its inline future at a stable address. Cloning the
+    // pinned Arc lets multiple tasks resume the same future without a separate future allocation.
+    let shared = Arc::pin(LazyCell::new(async || {
+        tokio::task::yield_now().await;
+        "shared".to_owned()
+    }));
+
+    let first = tokio::spawn({
+        let shared = shared.clone();
+        async move { LazyCell::force_pin(shared.as_ref()).await.clone() }
+    });
+    let second = tokio::spawn({
+        let shared = shared.clone();
+        async move { LazyCell::force_pin(shared.as_ref()).await.clone() }
+    });
+
+    assert_eq!(first.await.unwrap(), "shared");
+    assert_eq!(second.await.unwrap(), "shared");
+}
diff --git a/examples/src/once_cell_vs_lazy_cell.rs b/examples/src/once_cell_vs_lazy_cell.rs
index c546fdc..726a117 100644
--- a/examples/src/once_cell_vs_lazy_cell.rs
+++ b/examples/src/once_cell_vs_lazy_cell.rs
@@ -25,8 +25,7 @@ use asyncband::once::OnceCell;
 use tokio::sync::Notify;
 
 static ONCE_ENDPOINT: OnceCell<String> = OnceCell::new();
-static LAZY_ENDPOINT: LazyCell<String, fn() -> Ready<String>> =
-    LazyCell::new(load_default_endpoint);
+static LAZY_ENDPOINT: LazyCell<String, Ready<String>> = LazyCell::new(load_default_endpoint);
 
 fn load_default_endpoint() -> Ready<String> {
     std::future::ready("https://service.example".to_owned())
@@ -109,12 +108,12 @@ async fn lazy_cell_owns_a_local_fn_once() {
     // A caller could pass `initialize` to `OnceCell::get_or_init` once, but the call consumes it.
     // If that call is cancelled, a later caller cannot supply the same `FnOnce` again. `LazyCell`
     // owns the initializer and preserves its in-flight future across callers.
-    let client = Arc::new(LazyCell::<Client, _>::new(initialize));
+    let client = Arc::pin(LazyCell::new(initialize));
 
     let first_caller = tokio::spawn({
-        let client = Arc::clone(&client);
+        let client = client.clone();
         async move {
-            LazyCell::force(&client).await;
+            LazyCell::force_pin(client.as_ref()).await;
         }
     });
     started.notified().await;
@@ -124,6 +123,6 @@ async fn lazy_cell_owns_a_local_fn_once() {
     // Cancellation does not consume the captured credentials or restart the initializer. The next
     // caller resumes the same future.
     resume.notify_one();
-    assert_eq!(LazyCell::force(&client).await.token, "secret");
+    assert_eq!(LazyCell::force_pin(client.as_ref()).await.token, "secret");
     assert_eq!(attempts.load(Ordering::SeqCst), 1);
 }
diff --git a/tests-integration/tests/lazy_cell_test.rs b/tests-integration/tests/lazy_cell_test.rs
index 419e93e..b3bb9da 100644
--- a/tests-integration/tests/lazy_cell_test.rs
+++ b/tests-integration/tests/lazy_cell_test.rs
@@ -28,11 +28,11 @@ use tokio::sync::Notify;
 #[tokio::test]
 async fn initializer_starts_when_force_is_polled() {
     let attempts = Arc::new(AtomicUsize::new(0));
-    let lazy = LazyCell::<u32, _>::new({
+    let lazy = LazyCell::new({
         let attempts = attempts.clone();
         move || {
             attempts.fetch_add(1, Ordering::SeqCst);
-            async { 42 }
+            Box::pin(async { 42 })
         }
     });
 
@@ -48,12 +48,14 @@ async fn initializer_starts_when_force_is_polled() {
 #[tokio::test]
 async fn concurrent_force_runs_initializer_once() {
     let attempts = Arc::new(AtomicUsize::new(0));
-    let lazy = Arc::new(LazyCell::<u32, _>::new({
+    let lazy = Arc::new(LazyCell::new({
         let attempts = attempts.clone();
-        async move || {
-            attempts.fetch_add(1, Ordering::SeqCst);
-            tokio::task::yield_now().await;
-            42
+        move || {
+            Box::pin(async move {
+                attempts.fetch_add(1, Ordering::SeqCst);
+                tokio::task::yield_now().await;
+                42
+            })
         }
     }));
 
@@ -74,15 +76,17 @@ async fn cancellation_preserves_initialization_future() {
     let attempts = Arc::new(AtomicUsize::new(0));
     let started = Arc::new(Notify::new());
     let resume = Arc::new(Notify::new());
-    let lazy = Arc::new(LazyCell::<u32, _>::new({
+    let lazy = Arc::new(LazyCell::new({
         let attempts = attempts.clone();
         let started = started.clone();
         let resume = resume.clone();
-        async move || {
-            attempts.fetch_add(1, Ordering::SeqCst);
-            started.notify_one();
-            resume.notified().await;
-            42
+        move || {
+            Box::pin(async move {
+                attempts.fetch_add(1, Ordering::SeqCst);
+                started.notify_one();
+                resume.notified().await;
+                42
+            })
         }
     }));
 
@@ -111,9 +115,10 @@ async fn from_future_does_not_poll_until_forced() {
             42
         }
     });
+    let lazy = std::pin::pin!(lazy);
 
     assert_eq!(polls.load(Ordering::SeqCst), 0);
-    assert_eq!(LazyCell::force(&lazy).await, &42);
+    assert_eq!(LazyCell::force_pin(lazy.as_ref()).await, &42);
     assert_eq!(polls.load(Ordering::SeqCst), 1);
 }
 
@@ -122,7 +127,7 @@ async fn from_future_cancellation_preserves_the_same_future() {
     let executions = Arc::new(AtomicUsize::new(0));
     let started = Arc::new(Notify::new());
     let resume = Arc::new(Notify::new());
-    let lazy = Arc::new(LazyCell::from_future({
+    let lazy = Arc::pin(LazyCell::from_future({
         let executions = executions.clone();
         let started = started.clone();
         let resume = resume.clone();
@@ -136,14 +141,14 @@ async fn from_future_cancellation_preserves_the_same_future() {
 
     let task = {
         let lazy = lazy.clone();
-        tokio::spawn(async move { *LazyCell::force(&lazy).await })
+        tokio::spawn(async move { *LazyCell::force_pin(lazy.as_ref()).await })
     };
     started.notified().await;
     task.abort();
     assert!(task.await.unwrap_err().is_cancelled());
 
     resume.notify_one();
-    assert_eq!(*LazyCell::force(&lazy).await, 42);
+    assert_eq!(*LazyCell::force_pin(lazy.as_ref()).await, 42);
     assert_eq!(executions.load(Ordering::SeqCst), 1);
 }
 
@@ -151,13 +156,15 @@ async fn from_future_cancellation_preserves_the_same_future() {
 async fn unrelated_unwind_does_not_poison_pending_attempt() {
     let started = Arc::new(Notify::new());
     let resume = Arc::new(Notify::new());
-    let lazy = Arc::new(LazyCell::<u32, _>::new({
+    let lazy = Arc::new(LazyCell::new({
         let started = started.clone();
         let resume = resume.clone();
-        async move || {
-            started.notify_one();
-            resume.notified().await;
-            42
+        move || {
+            Box::pin(async move {
+                started.notify_one();
+                resume.notified().await;
+                42
+            })
         }
     }));
 
@@ -185,13 +192,15 @@ async fn dropping_cell_drops_suspended_attempt() {
     let held = Arc::new(());
     let weak = Arc::downgrade(&held);
     let started = Arc::new(Notify::new());
-    let lazy = Arc::new(LazyCell::<u32, _>::new({
+    let lazy = Arc::new(LazyCell::new({
         let started = started.clone();
-        async move || {
-            started.notify_one();
-            std::future::pending::<()>().await;
-            drop(held);
-            42
+        move || {
+            Box::pin(async move {
+                started.notify_one();
+                std::future::pending::<()>().await;
+                drop(held);
+                42
+            })
         }
     }));
 
@@ -210,11 +219,13 @@ async fn dropping_cell_drops_suspended_attempt() {
 #[tokio::test]
 async fn result_is_cached_as_the_value() {
     let attempts = Arc::new(AtomicUsize::new(0));
-    let lazy = LazyCell::<Result<u32, &'static str>, _>::new({
+    let lazy = LazyCell::new({
         let attempts = attempts.clone();
-        async move || {
-            attempts.fetch_add(1, Ordering::SeqCst);
-            Err("cached")
+        move || {
+            Box::pin(async move {
+                attempts.fetch_add(1, Ordering::SeqCst);
+                Err::<u32, _>("cached")
+            })
         }
     });
 
@@ -226,11 +237,13 @@ async fn result_is_cached_as_the_value() {
 #[tokio::test]
 async fn initializer_poll_panic_permanently_poisons_cell() {
     let attempts = Arc::new(AtomicUsize::new(0));
-    let lazy = Arc::new(LazyCell::<u32, _>::new({
+    let lazy = Arc::new(LazyCell::new({
         let attempts = attempts.clone();
-        async move || {
-            attempts.fetch_add(1, Ordering::SeqCst);
-            panic!("initializer panic");
+        move || {
+            Box::pin(async move {
+                attempts.fetch_add(1, Ordering::SeqCst);
+                panic!("initializer panic");
+            })
         }
     }));
 
@@ -261,7 +274,7 @@ async fn initializer_poll_panic_permanently_poisons_cell() {
 
 #[tokio::test]
 async fn initializer_creation_panic_permanently_poisons_cell() {
-    let lazy = Arc::new(LazyCell::<u32, _>::new(|| -> std::future::Ready<u32> {
+    let lazy = Arc::new(LazyCell::new(|| -> std::future::Ready<u32> {
         panic!("initializer creation panic")
     }));
 
@@ -281,7 +294,7 @@ async fn initializer_creation_panic_permanently_poisons_cell() {
 
 #[tokio::test]
 async fn force_mut_updates_value() {
-    let mut lazy = LazyCell::<u32, _>::new(async || 41);
+    let mut lazy = LazyCell::new(|| std::future::ready(41));
     *LazyCell::force_mut(&mut lazy).await += 1;
     assert_eq!(LazyCell::get(&lazy), Some(&42));
 }
@@ -290,13 +303,15 @@ async fn force_mut_updates_value() {
 async fn force_mut_resumes_a_started_attempt() {
     let started = Arc::new(Notify::new());
     let resume = Arc::new(Notify::new());
-    let lazy = Arc::new(LazyCell::<u32, _>::new({
+    let lazy = Arc::new(LazyCell::new({
         let started = started.clone();
         let resume = resume.clone();
-        async move || {
-            started.notify_one();
-            resume.notified().await;
-            42
+        move || {
+            Box::pin(async move {
+                started.notify_one();
+                resume.notified().await;
+                42
+            })
         }
     }));
 
@@ -317,12 +332,12 @@ async fn force_mut_resumes_a_started_attempt() {
 
 #[tokio::test]
 async fn default_and_value_constructors_match_lazy_cell() {
-    let lazy = LazyCell::<u32>::default();
+    let lazy = LazyCell::<u32, Ready<u32>>::default();
     assert_eq!(format!("{lazy:?}"), "LazyCell(<uninit>)");
     assert_eq!(LazyCell::force(&lazy).await, &0);
     assert_eq!(format!("{lazy:?}"), "LazyCell(0)");
 
-    let local = LazyCell::<Rc<u32>>::default();
+    let local = LazyCell::<Rc<u32>, Ready<Rc<u32>>>::default();
     assert_eq!(**LazyCell::force(&local).await, 0);
 
     let lazy = const { LazyCell::from_value(42) };
@@ -334,9 +349,11 @@ async fn initializer_need_not_be_sync() {
     fn assert_sync<T: Sync>(_: &T) {}
 
     let count = Cell::new(0);
-    let lazy = LazyCell::<u32, _>::new(async move || {
-        count.set(count.get() + 1);
-        count.get()
+    let lazy = LazyCell::new(move || {
+        Box::pin(async move {
+            count.set(count.get() + 1);
+            count.get()
+        })
     });
 
     assert_sync(&lazy);
@@ -347,9 +364,57 @@ fn static_initializer() -> Ready<u32> {
     std::future::ready(42)
 }
 
-static STATIC_LAZY: LazyCell<u32, fn() -> Ready<u32>> = LazyCell::new(static_initializer);
+static STATIC_LAZY: LazyCell<u32, Ready<u32>> = LazyCell::new(static_initializer);
 
 #[tokio::test]
 async fn function_pointer_initializer_supports_statics() {
     assert_eq!(LazyCell::force(&STATIC_LAZY).await, &42);
 }
+
+#[tokio::test]
+async fn inline_future_supports_local_non_send_state() {
+    let local = Rc::new(41);
+    let lazy = LazyCell::new({
+        let local = Rc::clone(&local);
+        async move || {
+            tokio::task::yield_now().await;
+            *local + 1
+        }
+    });
+    let mut lazy = std::pin::pin!(lazy);
+
+    assert_eq!(*LazyCell::force_pin(lazy.as_ref()).await, 42);
+    assert_eq!(LazyCell::get(&lazy), Some(&42));
+
+    *LazyCell::force_pin_mut(lazy.as_mut()).await += 1;
+    assert_eq!(LazyCell::get(&lazy), Some(&43));
+}
+
+#[tokio::test]
+async fn cancellation_preserves_inline_initialization_future() {
+    let attempts = Arc::new(AtomicUsize::new(0));
+    let started = Arc::new(Notify::new());
+    let resume = Arc::new(Notify::new());
+    let lazy = LazyCell::new({
+        let attempts = Arc::clone(&attempts);
+        let started = Arc::clone(&started);
+        let resume = Arc::clone(&resume);
+        async move || {
+            attempts.fetch_add(1, Ordering::SeqCst);
+            started.notify_one();
+            resume.notified().await;
+            42
+        }
+    });
+    let lazy = std::pin::pin!(lazy);
+
+    tokio::select! {
+        biased;
+        _ = LazyCell::force_pin(lazy.as_ref()) => panic!("initializer unexpectedly completed"),
+        _ = started.notified() => {}
+    }
+
+    resume.notify_one();
+    assert_eq!(*LazyCell::force_pin(lazy.as_ref()).await, 42);
+    assert_eq!(attempts.load(Ordering::SeqCst), 1);
+}
diff --git a/tests-integration/tests/traits_test.rs b/tests-integration/tests/traits_test.rs
index 7872e29..012ae35 100644
--- a/tests-integration/tests/traits_test.rs
+++ b/tests-integration/tests/traits_test.rs
@@ -68,7 +68,7 @@ fn public_types_are_send_and_sync() {
 
     assert_send_and_sync::<Barrier>();
     assert_send_and_sync::<Condvar>();
-    assert_send_and_sync::<LazyCell<u32>>();
+    assert_send_and_sync::<LazyCell<u32, std::future::Ready<u32>>>();
     assert_send_and_sync::<Once>();
     assert_send_and_sync::<OnceCell<u32>>();
     assert_send_and_sync::<OnceMap<String, u32>>();
@@ -116,7 +116,7 @@ fn public_types_are_unpin() {
     assert_unpin::<Barrier>();
     assert_unpin::<Condvar>();
     assert_unpin::<Latch>();
-    assert_unpin::<LazyCell<u32>>();
+    assert_unpin::<LazyCell<u32, std::future::Ready<u32>>>();
     assert_unpin::<Once>();
     assert_unpin::<OnceCell<u32>>();
     assert_unpin::<OnceMap<String, u32>>();


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@asyncband.apache.org
For additional commands, e-mail: commits-help@asyncband.apache.org