Skip to main content

texl_core/
helpers.rs

1//! Glue between Swan promises and the host's async runtime.
2//!
3//! The engine runs imports as background tasks. It does not depend on any
4//! runtime: the host passes a [`SpawnFn`] that starts a [`Task`] on its own
5//! executor. Without one, tasks go onto a thread-local queue that the host
6//! drains with [`run_pending`].
7//!
8//! [`promise_async`] is also useful to hosts: it turns a Rust future into a
9//! Swan promise, so a host function can do asynchronous work.
10//!
11//! # Examples
12//!
13//! A host function that returns a promise:
14//!
15//! ```
16//! use swan_core::value::{BuiltinFn, Value};
17//! use texl_core::{Engine, helpers::promise_async};
18//!
19//! let engine = Engine::new(None);
20//! engine.add_builtin("fetch_answer", Value::Builtin(BuiltinFn::new("fetch_answer", |_| {
21//!     promise_async(None, async { Value::Numb(42.0) })
22//! })));
23//! assert_eq!(block_on(engine.render("<% fetch_answer() %>")), "42");
24//! # use std::{future::Future, pin::pin, task::{Context, Poll, Waker}};
25//! # fn block_on<F: Future>(fut: F) -> F::Output {
26//! #     let mut cx = Context::from_waker(Waker::noop());
27//! #     let mut fut = pin!(fut);
28//! #     loop {
29//! #         texl_core::helpers::run_pending();
30//! #         if let Poll::Ready(v) = fut.as_mut().poll(&mut cx) { return v; }
31//! #     }
32//! # }
33//! ```
34
35use std::cell::RefCell;
36use std::collections::VecDeque;
37use std::future::Future;
38use std::pin::Pin;
39use std::rc::Rc;
40use std::task::{Context, Waker};
41
42use swan_core::value::{Promise, Value};
43
44/// A background task, as handed to a spawner. It is not `Send`: tasks run on the thread that created them.
45pub type Task = Pin<Box<dyn Future<Output = ()>>>;
46
47/// A host-provided function that runs a background task to completion.
48pub type SpawnFn = Rc<dyn Fn(Task)>;
49
50/// Builds a [`SpawnFn`] from a plain closure.
51pub fn spawner_fn(f: impl Fn(Task) + 'static) -> SpawnFn {
52    Rc::new(f)
53}
54
55thread_local! {
56    /// Queue of tasks spawned via [`spawn_default`], awaiting [`run_pending`].
57    static LOCAL_TASKS: RefCell<VecDeque<Task>> = RefCell::new(VecDeque::new());
58}
59
60/// Fallback spawner: queues the task on a thread-local queue.
61///
62/// The task does not run until [`run_pending`] is called.
63pub fn spawn_default(task: Task) {
64    LOCAL_TASKS.with(|q| q.borrow_mut().push_back(task));
65}
66
67/// Polls queued fallback tasks until no further progress is possible.
68///
69/// Uses a no-op waker, so it cannot be woken by external events. It stops when a full
70/// pass completes no task and spawns no new one.
71pub fn run_pending() {
72    let mut cx = Context::from_waker(Waker::noop());
73    loop {
74        let batch: VecDeque<Task> = LOCAL_TASKS.with(|q| std::mem::take(&mut *q.borrow_mut()));
75        if batch.is_empty() {
76            break;
77        }
78        let mut progressed = false;
79        let mut still_pending = 0;
80        for mut task in batch {
81            if task.as_mut().poll(&mut cx).is_ready() {
82                progressed = true;
83            } else {
84                still_pending += 1;
85                LOCAL_TASKS.with(|q| q.borrow_mut().push_back(task));
86            }
87        }
88        let queued = LOCAL_TASKS.with(|q| q.borrow().len());
89        if !progressed && queued == still_pending {
90            break;
91        }
92    }
93}
94
95/// Runs `task` with `spawner`, or queues it with [`spawn_default`] if `spawner` is `None`.
96pub fn spawn(spawner: Option<&SpawnFn>, task: Task) {
97    match spawner {
98        Some(s) => s(task),
99        None => spawn_default(task),
100    }
101}
102
103/// Wraps an async operation in a background task that resolves a Swan [`Promise`].
104///
105/// Executes `fut` inside a background task spawned via [`spawn`]. Returns immediately
106/// with a [`Value::Promise`]. Once `fut` completes, the returned promise resolves to its value.
107pub fn promise_async<F>(spawner: Option<&SpawnFn>, fut: F) -> Value
108where
109    F: Future<Output = Value> + 'static,
110{
111    let promise = Promise::new();
112    let promise_val = Value::Promise(promise.clone());
113
114    let task_promise = promise;
115    spawn(
116        spawner,
117        Box::pin(async move {
118            let result = fut.await;
119            task_promise.resolve(result);
120        }),
121    );
122
123    promise_val
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use std::cell::RefCell;
130
131    #[test]
132    fn test_promise_async_resolves_value() {
133        let val = promise_async(None, async { Value::Numb(42.0) });
134
135        assert!(matches!(val, Value::Promise(_)));
136
137        if let Value::Promise(p) = val {
138            let slot: Rc<RefCell<Option<Value>>> = Rc::new(RefCell::new(None));
139            let slot_clone = slot.clone();
140            p.then(move |v| {
141                *slot_clone.borrow_mut() = Some(v);
142                Value::Nothing
143            });
144
145            run_pending();
146            assert_eq!(*slot.borrow(), Some(Value::Numb(42.0)));
147        }
148    }
149}