Skip to main content

sim_lib_lang_javascript/
jobs.rs

1//! ECMAScript resumable control and explicitly driven reaction jobs.
2
3use std::{cell::RefCell, rc::Rc};
4
5use sim_lib_control::{
6    AdmissionLimit, CheckpointError, CheckpointReceipt, FrameError, FrameLimits, JobId, JobQueues,
7    ResumableFrame, ResumePacket, ResumeResult, RuntimeJobClass, WorkLimit,
8};
9
10use crate::JavascriptValue;
11
12/// JavaScript exception value carried by shared resume packets.
13#[derive(Clone, Debug, PartialEq)]
14pub struct JavascriptException {
15    /// Stable exception name such as `TypeError`.
16    pub name: String,
17    /// Human-readable exception message.
18    pub message: String,
19}
20
21impl JavascriptException {
22    /// Creates an exception without consulting ambient host state.
23    pub fn new(name: impl Into<String>, message: impl Into<String>) -> Self {
24        Self {
25            name: name.into(),
26            message: message.into(),
27        }
28    }
29}
30
31/// Queue classes used by the JavaScript profile.
32#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
33pub enum JavascriptJobClass {
34    /// Promise reactions, async continuations, and dynamic-import evaluation.
35    Microtask,
36    /// Collector-admitted finalization work, never drained by a microtask checkpoint.
37    Finalization,
38}
39
40impl From<JavascriptJobClass> for RuntimeJobClass {
41    fn from(value: JavascriptJobClass) -> Self {
42        match value {
43            JavascriptJobClass::Microtask => RuntimeJobClass::LanguageMicrotask("javascript"),
44            JavascriptJobClass::Finalization => RuntimeJobClass::Finalization,
45        }
46    }
47}
48
49/// Explicit JavaScript job organ. It owns no thread, timer, or host event loop.
50pub struct JavascriptJobs {
51    queues: JobQueues<RuntimeJobClass>,
52}
53
54impl JavascriptJobs {
55    /// Creates queues under a lifetime admission limit.
56    pub fn new(admission: AdmissionLimit) -> Self {
57        Self {
58            queues: JobQueues::new(admission),
59        }
60    }
61
62    /// Admits a promise/module reaction into the JavaScript microtask FIFO.
63    pub fn enqueue_microtask(
64        &mut self,
65        job: impl FnOnce(&mut JobQueues<RuntimeJobClass>) + 'static,
66    ) -> Result<JobId, CheckpointError> {
67        self.queues
68            .enqueue(RuntimeJobClass::LanguageMicrotask("javascript"), job)
69            .map(|receipt| receipt.id)
70    }
71
72    /// Admits collector finalization independently of JavaScript microtasks.
73    pub fn enqueue_finalization(
74        &mut self,
75        job: impl FnOnce(&mut JobQueues<RuntimeJobClass>) + 'static,
76    ) -> Result<JobId, CheckpointError> {
77        self.queues
78            .enqueue(RuntimeJobClass::Finalization, job)
79            .map(|receipt| receipt.id)
80    }
81
82    /// Cancels queued work before an explicit checkpoint.
83    pub fn cancel(&mut self, id: JobId) {
84        self.queues.cancel(id);
85    }
86
87    /// Drains the JavaScript microtask class to empty, including reentrant reactions.
88    pub fn microtask_checkpoint(
89        &mut self,
90        work: WorkLimit,
91    ) -> Result<CheckpointReceipt<RuntimeJobClass>, CheckpointError> {
92        self.queues
93            .checkpoint(RuntimeJobClass::LanguageMicrotask("javascript"), work)
94    }
95
96    /// Explicitly drains collector finalization without touching microtasks.
97    pub fn finalization_checkpoint(
98        &mut self,
99        work: WorkLimit,
100    ) -> Result<CheckpointReceipt<RuntimeJobClass>, CheckpointError> {
101        self.queues.checkpoint(RuntimeJobClass::Finalization, work)
102    }
103}
104
105/// Promise state observed through a stable shared cell.
106#[derive(Clone, Debug, PartialEq)]
107pub enum JavascriptPromiseState {
108    /// No settlement has occurred.
109    Pending,
110    /// Fulfilled with one value.
111    Fulfilled(JavascriptValue),
112    /// Rejected with one exception.
113    Rejected(JavascriptException),
114}
115
116/// A promise whose reactions are admitted only to explicit JavaScript jobs.
117#[derive(Clone)]
118pub struct JavascriptPromise(Rc<RefCell<JavascriptPromiseState>>);
119
120impl Default for JavascriptPromise {
121    fn default() -> Self {
122        Self(Rc::new(RefCell::new(JavascriptPromiseState::Pending)))
123    }
124}
125
126impl JavascriptPromise {
127    /// Snapshots the current settlement state.
128    pub fn state(&self) -> JavascriptPromiseState {
129        self.0.borrow().clone()
130    }
131
132    /// Enqueues first-settlement fulfillment as a reaction job.
133    pub fn resolve(
134        &self,
135        jobs: &mut JavascriptJobs,
136        value: JavascriptValue,
137    ) -> Result<JobId, CheckpointError> {
138        let state = Rc::clone(&self.0);
139        jobs.enqueue_microtask(move |_| {
140            let mut state = state.borrow_mut();
141            if matches!(*state, JavascriptPromiseState::Pending) {
142                *state = JavascriptPromiseState::Fulfilled(value);
143            }
144        })
145    }
146
147    /// Enqueues first-settlement rejection as a reaction job.
148    pub fn reject(
149        &self,
150        jobs: &mut JavascriptJobs,
151        error: JavascriptException,
152    ) -> Result<JobId, CheckpointError> {
153        let state = Rc::clone(&self.0);
154        jobs.enqueue_microtask(move |_| {
155            let mut state = state.borrow_mut();
156            if matches!(*state, JavascriptPromiseState::Pending) {
157                *state = JavascriptPromiseState::Rejected(error);
158            }
159        })
160    }
161}
162
163/// Generator composed from the shared bounded resumable frame.
164pub struct JavascriptGenerator {
165    frame: ResumableFrame<Box<JavascriptResumeDriver>>,
166}
167
168type JavascriptResumeDriver = dyn FnMut(
169    ResumePacket<JavascriptValue, JavascriptException>,
170    &mut sim_lib_control::StepBudget,
171) -> Result<
172    ResumeResult<JavascriptValue, JavascriptValue, JavascriptException>,
173    FrameError,
174>;
175
176impl JavascriptGenerator {
177    /// Creates a bounded generator from JavaScript policy dispatch.
178    pub fn new(
179        limits: FrameLimits,
180        dispatch: impl FnMut(
181            ResumePacket<JavascriptValue, JavascriptException>,
182            &mut sim_lib_control::StepBudget,
183        ) -> Result<
184            ResumeResult<JavascriptValue, JavascriptValue, JavascriptException>,
185            FrameError,
186        > + 'static,
187    ) -> Self {
188        Self {
189            frame: ResumableFrame::new(limits, Box::new(dispatch) as Box<JavascriptResumeDriver>),
190        }
191    }
192
193    /// Resumes with `next`, `throw`, or `return` represented by a shared packet.
194    pub fn resume(
195        &mut self,
196        packet: ResumePacket<JavascriptValue, JavascriptException>,
197    ) -> Result<ResumeResult<JavascriptValue, JavascriptValue, JavascriptException>, FrameError>
198    {
199        self.frame.resume(packet)
200    }
201}
202
203/// Async function execution is a generator whose terminal result settles a promise.
204pub struct JavascriptAsyncFunction {
205    generator: JavascriptGenerator,
206    promise: JavascriptPromise,
207}
208
209impl JavascriptAsyncFunction {
210    /// Wraps a resumable body and its externally visible promise.
211    pub fn new(generator: JavascriptGenerator) -> Self {
212        Self {
213            generator,
214            promise: JavascriptPromise::default(),
215        }
216    }
217
218    /// Promise settled by explicit calls to [`Self::resume`].
219    pub fn promise(&self) -> JavascriptPromise {
220        self.promise.clone()
221    }
222
223    /// Advances the body and admits terminal settlement to the microtask queue.
224    pub fn resume(
225        &mut self,
226        packet: ResumePacket<JavascriptValue, JavascriptException>,
227        jobs: &mut JavascriptJobs,
228    ) -> Result<ResumeResult<JavascriptValue, JavascriptValue, JavascriptException>, FrameError>
229    {
230        let result = self.generator.resume(packet)?;
231        match &result {
232            ResumeResult::Returned(value) => {
233                self.promise
234                    .resolve(jobs, value.clone())
235                    .map_err(|_| FrameError::WorkExhausted)?;
236            }
237            ResumeResult::Failed(error) => {
238                self.promise
239                    .reject(jobs, error.clone())
240                    .map_err(|_| FrameError::WorkExhausted)?;
241            }
242            ResumeResult::Yielded(_) => {}
243        }
244        Ok(result)
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use std::{cell::RefCell, rc::Rc};
251
252    use super::*;
253
254    #[test]
255    fn generator_exception_and_async_settlement_use_shared_packets() {
256        let mut generator =
257            JavascriptGenerator::new(FrameLimits { depth: 2, work: 4 }, |packet, budget| {
258                budget.charge_work()?;
259                Ok(match packet {
260                    ResumePacket::Start => ResumeResult::Yielded(JavascriptValue::Number(1.0)),
261                    ResumePacket::Send(value) => ResumeResult::Returned(value),
262                    ResumePacket::Throw(error) => ResumeResult::Failed(error),
263                    ResumePacket::Close => ResumeResult::Returned(JavascriptValue::Undefined),
264                })
265            });
266        assert!(matches!(
267            generator.resume(ResumePacket::Start).unwrap(),
268            ResumeResult::Yielded(JavascriptValue::Number(1.0))
269        ));
270        let error = JavascriptException::new("RangeError", "bounded");
271        assert_eq!(
272            generator
273                .resume(ResumePacket::Throw(error.clone()))
274                .unwrap(),
275            ResumeResult::Failed(error)
276        );
277
278        let generator = JavascriptGenerator::new(FrameLimits { depth: 1, work: 1 }, |_, _| {
279            Ok(ResumeResult::Returned(JavascriptValue::Number(42.0)))
280        });
281        let mut function = JavascriptAsyncFunction::new(generator);
282        let promise = function.promise();
283        let mut jobs = JavascriptJobs::new(AdmissionLimit(4));
284        function.resume(ResumePacket::Start, &mut jobs).unwrap();
285        assert_eq!(promise.state(), JavascriptPromiseState::Pending);
286        jobs.microtask_checkpoint(WorkLimit(4)).unwrap();
287        assert_eq!(
288            promise.state(),
289            JavascriptPromiseState::Fulfilled(JavascriptValue::Number(42.0))
290        );
291    }
292
293    #[test]
294    fn checkpoint_drains_reentrant_microtasks_and_isolates_finalization() {
295        let trace = Rc::new(RefCell::new(Vec::new()));
296        let mut jobs = JavascriptJobs::new(AdmissionLimit(8));
297        let outer_trace = Rc::clone(&trace);
298        jobs.enqueue_microtask(move |queues| {
299            outer_trace.borrow_mut().push("reaction-1");
300            let inner_trace = Rc::clone(&outer_trace);
301            queues
302                .enqueue(
303                    RuntimeJobClass::LanguageMicrotask("javascript"),
304                    move |_| {
305                        inner_trace.borrow_mut().push("reaction-2");
306                    },
307                )
308                .unwrap();
309        })
310        .unwrap();
311        let final_trace = Rc::clone(&trace);
312        jobs.enqueue_finalization(move |_| final_trace.borrow_mut().push("finalize"))
313            .unwrap();
314        let receipt = jobs.microtask_checkpoint(WorkLimit(3)).unwrap();
315        assert_eq!(receipt.completed.len(), 2);
316        assert_eq!(&*trace.borrow(), &["reaction-1", "reaction-2"]);
317        jobs.finalization_checkpoint(WorkLimit(1)).unwrap();
318        assert_eq!(&*trace.borrow(), &["reaction-1", "reaction-2", "finalize"]);
319    }
320
321    #[test]
322    fn exhaustion_and_cancellation_fail_closed_without_implicit_work() {
323        let ran = Rc::new(RefCell::new(0));
324        let mut jobs = JavascriptJobs::new(AdmissionLimit(2));
325        let first_ran = Rc::clone(&ran);
326        let first = jobs
327            .enqueue_microtask(move |_| *first_ran.borrow_mut() += 1)
328            .unwrap();
329        let second_ran = Rc::clone(&ran);
330        jobs.enqueue_microtask(move |_| *second_ran.borrow_mut() += 1)
331            .unwrap();
332        assert_eq!(*ran.borrow(), 0);
333        assert_eq!(
334            jobs.enqueue_microtask(|_| {}).unwrap_err(),
335            CheckpointError::AdmissionExhausted
336        );
337        jobs.cancel(first);
338        assert_eq!(
339            jobs.microtask_checkpoint(WorkLimit(1)).unwrap_err(),
340            CheckpointError::WorkExhausted
341        );
342        assert_eq!(*ran.borrow(), 0);
343        jobs.microtask_checkpoint(WorkLimit(1)).unwrap();
344        assert_eq!(*ran.borrow(), 1);
345    }
346}