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}