1use std::{cell::RefCell, collections::BTreeMap, fmt, rc::Rc, sync::Arc};
4
5use sim_kernel::{ClassRef, Cx, Object, ObjectCompat, Origin, Result as KernelResult, Symbol};
6
7use sim_lib_control::{
8 AdmissionLimit, CheckpointError, CheckpointReceipt, FrameError, FrameLimits, JobId, JobQueues,
9 Raised, ResumableFrame, ResumePacket, ResumeResult, RuntimeJobClass, WorkLimit,
10};
11
12use crate::JavascriptValue;
13
14#[derive(Debug)]
15struct JavascriptThrownFace {
16 value: JavascriptValue,
17}
18impl Object for JavascriptThrownFace {
19 fn display(&self, _cx: &mut Cx) -> KernelResult<String> {
20 Ok(format!("{:?}", self.value))
21 }
22 fn as_any(&self) -> &dyn std::any::Any {
23 self
24 }
25}
26impl ObjectCompat for JavascriptThrownFace {}
27
28pub struct JavascriptExceptionRealm {
33 undefined_class: ClassRef,
34 null_class: ClassRef,
35 boolean_class: ClassRef,
36 number_class: ClassRef,
37 bigint_class: ClassRef,
38 string_class: ClassRef,
39 object_class: ClassRef,
40 managed_classes: BTreeMap<sim_lib_mutation::ManagedId, ClassRef>,
41}
42
43impl fmt::Debug for JavascriptExceptionRealm {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.debug_struct("JavascriptExceptionRealm")
46 .field("managed_class_count", &self.managed_classes.len())
47 .finish_non_exhaustive()
48 }
49}
50
51impl JavascriptExceptionRealm {
52 #[allow(clippy::too_many_arguments)]
54 pub fn new(
55 undefined_class: ClassRef,
56 null_class: ClassRef,
57 boolean_class: ClassRef,
58 number_class: ClassRef,
59 bigint_class: ClassRef,
60 string_class: ClassRef,
61 object_class: ClassRef,
62 ) -> Self {
63 Self {
64 undefined_class,
65 null_class,
66 boolean_class,
67 number_class,
68 bigint_class,
69 string_class,
70 object_class,
71 managed_classes: BTreeMap::new(),
72 }
73 }
74
75 pub fn register_managed_class(
77 &mut self,
78 object: sim_lib_mutation::ManagedHandle,
79 class: ClassRef,
80 ) {
81 self.managed_classes.insert(object.id(), class);
82 }
83
84 pub fn raise(&self, cx: &Cx, value: JavascriptValue, origin: Origin) -> KernelResult<Raised> {
86 let class = match &value {
87 JavascriptValue::Undefined => self.undefined_class.clone(),
88 JavascriptValue::Null => self.null_class.clone(),
89 JavascriptValue::Bool(_) => self.boolean_class.clone(),
90 JavascriptValue::Number(_) => self.number_class.clone(),
91 JavascriptValue::BigInt(_) => self.bigint_class.clone(),
92 JavascriptValue::String(_) => self.string_class.clone(),
93 JavascriptValue::Managed(handle) => self
94 .managed_classes
95 .get(&handle.id())
96 .cloned()
97 .unwrap_or_else(|| self.object_class.clone()),
98 };
99 let payload = cx
100 .factory()
101 .opaque(Arc::new(JavascriptThrownFace { value }))?;
102 Raised::new(
103 class,
104 payload,
105 origin,
106 Symbol::qualified("javascript", "exception"),
107 )
108 }
109
110 pub fn thrown_value<'a>(&self, raised: &'a Raised) -> Option<&'a JavascriptValue> {
112 (raised.profile() == &Symbol::qualified("javascript", "exception"))
113 .then(|| {
114 raised
115 .payload()
116 .object()
117 .downcast_ref::<JavascriptThrownFace>()
118 .map(|face| &face.value)
119 })
120 .flatten()
121 }
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
126pub enum JavascriptJobClass {
127 Microtask,
129 Finalization,
131}
132
133impl From<JavascriptJobClass> for RuntimeJobClass {
134 fn from(value: JavascriptJobClass) -> Self {
135 match value {
136 JavascriptJobClass::Microtask => RuntimeJobClass::LanguageMicrotask("javascript"),
137 JavascriptJobClass::Finalization => RuntimeJobClass::Finalization,
138 }
139 }
140}
141
142pub struct JavascriptJobs {
144 queues: JobQueues<RuntimeJobClass>,
145}
146
147impl JavascriptJobs {
148 pub fn new(admission: AdmissionLimit) -> Self {
150 Self {
151 queues: JobQueues::new(admission),
152 }
153 }
154
155 pub fn enqueue_microtask(
157 &mut self,
158 job: impl FnOnce(&mut JobQueues<RuntimeJobClass>) + 'static,
159 ) -> Result<JobId, CheckpointError> {
160 self.queues
161 .enqueue(RuntimeJobClass::LanguageMicrotask("javascript"), job)
162 .map(|receipt| receipt.id)
163 }
164
165 pub fn enqueue_finalization(
167 &mut self,
168 job: impl FnOnce(&mut JobQueues<RuntimeJobClass>) + 'static,
169 ) -> Result<JobId, CheckpointError> {
170 self.queues
171 .enqueue(RuntimeJobClass::Finalization, job)
172 .map(|receipt| receipt.id)
173 }
174
175 pub fn cancel(&mut self, id: JobId) {
177 self.queues.cancel(id);
178 }
179
180 pub fn microtask_checkpoint(
182 &mut self,
183 work: WorkLimit,
184 ) -> Result<CheckpointReceipt<RuntimeJobClass>, CheckpointError> {
185 self.queues
186 .checkpoint(RuntimeJobClass::LanguageMicrotask("javascript"), work)
187 }
188
189 pub fn finalization_checkpoint(
191 &mut self,
192 work: WorkLimit,
193 ) -> Result<CheckpointReceipt<RuntimeJobClass>, CheckpointError> {
194 self.queues.checkpoint(RuntimeJobClass::Finalization, work)
195 }
196}
197
198#[derive(Clone, Debug, PartialEq)]
200pub enum JavascriptPromiseState {
201 Pending,
203 Fulfilled(JavascriptValue),
205 Rejected(Raised),
207}
208
209#[derive(Clone)]
211pub struct JavascriptPromise(Rc<RefCell<JavascriptPromiseState>>);
212
213impl Default for JavascriptPromise {
214 fn default() -> Self {
215 Self(Rc::new(RefCell::new(JavascriptPromiseState::Pending)))
216 }
217}
218
219impl JavascriptPromise {
220 pub fn state(&self) -> JavascriptPromiseState {
222 self.0.borrow().clone()
223 }
224
225 pub fn resolve(
227 &self,
228 jobs: &mut JavascriptJobs,
229 value: JavascriptValue,
230 ) -> Result<JobId, CheckpointError> {
231 let state = Rc::clone(&self.0);
232 jobs.enqueue_microtask(move |_| {
233 let mut state = state.borrow_mut();
234 if matches!(*state, JavascriptPromiseState::Pending) {
235 *state = JavascriptPromiseState::Fulfilled(value);
236 }
237 })
238 }
239
240 pub fn reject(
242 &self,
243 jobs: &mut JavascriptJobs,
244 error: Raised,
245 ) -> Result<JobId, CheckpointError> {
246 let state = Rc::clone(&self.0);
247 jobs.enqueue_microtask(move |_| {
248 let mut state = state.borrow_mut();
249 if matches!(*state, JavascriptPromiseState::Pending) {
250 *state = JavascriptPromiseState::Rejected(error);
251 }
252 })
253 }
254}
255
256pub struct JavascriptGenerator {
258 frame: ResumableFrame<Box<JavascriptResumeDriver>>,
259}
260
261type JavascriptResumeDriver =
262 dyn FnMut(
263 ResumePacket<JavascriptValue, Raised>,
264 &mut sim_lib_control::StepBudget,
265 ) -> Result<ResumeResult<JavascriptValue, JavascriptValue, Raised>, FrameError>;
266
267impl JavascriptGenerator {
268 pub fn new(
270 limits: FrameLimits,
271 dispatch: impl FnMut(
272 ResumePacket<JavascriptValue, Raised>,
273 &mut sim_lib_control::StepBudget,
274 ) -> Result<
275 ResumeResult<JavascriptValue, JavascriptValue, Raised>,
276 FrameError,
277 > + 'static,
278 ) -> Self {
279 Self {
280 frame: ResumableFrame::new(limits, Box::new(dispatch) as Box<JavascriptResumeDriver>),
281 }
282 }
283
284 pub fn resume(
286 &mut self,
287 packet: ResumePacket<JavascriptValue, Raised>,
288 ) -> Result<ResumeResult<JavascriptValue, JavascriptValue, Raised>, FrameError> {
289 self.frame.resume(packet)
290 }
291}
292
293pub struct JavascriptAsyncFunction {
295 generator: JavascriptGenerator,
296 promise: JavascriptPromise,
297}
298
299impl JavascriptAsyncFunction {
300 pub fn new(generator: JavascriptGenerator) -> Self {
302 Self {
303 generator,
304 promise: JavascriptPromise::default(),
305 }
306 }
307
308 pub fn promise(&self) -> JavascriptPromise {
310 self.promise.clone()
311 }
312
313 pub fn resume(
315 &mut self,
316 packet: ResumePacket<JavascriptValue, Raised>,
317 jobs: &mut JavascriptJobs,
318 ) -> Result<ResumeResult<JavascriptValue, JavascriptValue, Raised>, FrameError> {
319 let result = self.generator.resume(packet)?;
320 match &result {
321 ResumeResult::Returned(value) => {
322 self.promise
323 .resolve(jobs, value.clone())
324 .map_err(|_| FrameError::WorkExhausted)?;
325 }
326 ResumeResult::Failed(error) => {
327 self.promise
328 .reject(jobs, error.clone())
329 .map_err(|_| FrameError::WorkExhausted)?;
330 }
331 ResumeResult::Yielded(_) => {}
332 }
333 Ok(result)
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use std::{cell::RefCell, rc::Rc};
340
341 use super::*;
342 use sim_kernel::{ClassId, CodecId, SourceId, Span};
343
344 fn class(cx: &Cx, id: u32, name: &str) -> ClassRef {
345 cx.factory()
346 .class_stub(ClassId(id), Symbol::qualified("javascript", name))
347 .unwrap()
348 }
349
350 fn origin(at: usize) -> Origin {
351 Origin {
352 codec: CodecId(1),
353 source: SourceId("exceptions3-javascript".into()),
354 span: Span {
355 start: at,
356 end: at + 1,
357 },
358 trivia: Default::default(),
359 }
360 }
361
362 fn realm(cx: &Cx) -> JavascriptExceptionRealm {
363 JavascriptExceptionRealm::new(
364 class(cx, 1, "Undefined"),
365 class(cx, 2, "Null"),
366 class(cx, 3, "Boolean"),
367 class(cx, 4, "Number"),
368 class(cx, 5, "BigInt"),
369 class(cx, 6, "String"),
370 class(cx, 7, "Object"),
371 )
372 }
373
374 fn content_id(value: &str) -> u64 {
375 value.bytes().fold(0xcbf29ce484222325, |hash, byte| {
376 (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
377 })
378 }
379
380 fn exception_characterization() -> Vec<String> {
381 let cx = sim_kernel::testing::bare_cx();
382 let realm = realm(&cx);
383 let errors = [
384 JavascriptValue::Number(3.0),
385 JavascriptValue::String("plain throw".into()),
386 JavascriptValue::Undefined,
387 ];
388 let raised = errors
389 .iter()
390 .cloned()
391 .enumerate()
392 .map(|(at, value)| realm.raise(&cx, value, origin(at)).unwrap())
393 .collect::<Vec<_>>();
394 vec![
395 format!("number={:?}", realm.thrown_value(&raised[0])),
396 format!("string={:?}", realm.thrown_value(&raised[1])),
397 format!("undefined={:?}", realm.thrown_value(&raised[2])),
398 format!(
399 "origins={:?}",
400 raised.iter().map(Raised::origin).collect::<Vec<_>>()
401 ),
402 ]
403 }
404
405 #[test]
406 fn characterize_1_exception_behavior_replays_identically() {
407 let first = exception_characterization();
408 let replay = exception_characterization();
409 assert_eq!(first, replay);
410 assert_eq!(
411 first.iter().map(|row| content_id(row)).collect::<Vec<_>>(),
412 replay.iter().map(|row| content_id(row)).collect::<Vec<_>>()
413 );
414 }
415
416 #[test]
417 fn generator_exception_and_async_settlement_use_shared_packets() {
418 let cx = sim_kernel::testing::bare_cx();
419 let realm = realm(&cx);
420 let mut generator =
421 JavascriptGenerator::new(FrameLimits { depth: 2, work: 4 }, |packet, budget| {
422 budget.charge_work()?;
423 Ok(match packet {
424 ResumePacket::Start => ResumeResult::Yielded(JavascriptValue::Number(1.0)),
425 ResumePacket::Send(value) => ResumeResult::Returned(value),
426 ResumePacket::Throw(error) => ResumeResult::Failed(error),
427 ResumePacket::Close => ResumeResult::Returned(JavascriptValue::Undefined),
428 })
429 });
430 assert!(matches!(
431 generator.resume(ResumePacket::Start).unwrap(),
432 ResumeResult::Yielded(JavascriptValue::Number(1.0))
433 ));
434 let error = realm
435 .raise(&cx, JavascriptValue::String("bounded".into()), origin(3))
436 .unwrap();
437 assert_eq!(
438 generator
439 .resume(ResumePacket::Throw(error.clone()))
440 .unwrap(),
441 ResumeResult::Failed(error)
442 );
443
444 let generator = JavascriptGenerator::new(FrameLimits { depth: 1, work: 1 }, |_, _| {
445 Ok(ResumeResult::Returned(JavascriptValue::Number(42.0)))
446 });
447 let mut function = JavascriptAsyncFunction::new(generator);
448 let promise = function.promise();
449 let mut jobs = JavascriptJobs::new(AdmissionLimit(4));
450 function.resume(ResumePacket::Start, &mut jobs).unwrap();
451 assert_eq!(promise.state(), JavascriptPromiseState::Pending);
452 jobs.microtask_checkpoint(WorkLimit(4)).unwrap();
453 assert_eq!(
454 promise.state(),
455 JavascriptPromiseState::Fulfilled(JavascriptValue::Number(42.0))
456 );
457
458 let rejected = JavascriptPromise::default();
459 let thrown = realm
460 .raise(&cx, JavascriptValue::Number(1.0), origin(4))
461 .unwrap();
462 rejected.reject(&mut jobs, thrown.clone()).unwrap();
463 jobs.microtask_checkpoint(WorkLimit(1)).unwrap();
464 assert_eq!(rejected.state(), JavascriptPromiseState::Rejected(thrown));
465
466 let async_error = realm
467 .raise(&cx, JavascriptValue::Undefined, origin(5))
468 .unwrap();
469 let failed = async_error.clone();
470 let generator = JavascriptGenerator::new(FrameLimits { depth: 1, work: 1 }, move |_, _| {
471 Ok(ResumeResult::Failed(failed.clone()))
472 });
473 let mut function = JavascriptAsyncFunction::new(generator);
474 let promise = function.promise();
475 assert_eq!(
476 function.resume(ResumePacket::Start, &mut jobs).unwrap(),
477 ResumeResult::Failed(async_error.clone())
478 );
479 jobs.microtask_checkpoint(WorkLimit(1)).unwrap();
480 assert_eq!(
481 promise.state(),
482 JavascriptPromiseState::Rejected(async_error)
483 );
484 }
485
486 #[test]
487 fn arbitrary_throws_keep_values_and_realm_class_identity() {
488 let cx = sim_kernel::testing::bare_cx();
489 let mut realm = realm(&cx);
490 let mut heap = crate::JavascriptHeap::retaining(2).unwrap();
491 let plain = heap
492 .allocate(crate::JavascriptManagedObject::new(
493 crate::JavascriptManagedKind::Object,
494 ))
495 .unwrap();
496 let subclassed_error = heap
497 .allocate(crate::JavascriptManagedObject::new(
498 crate::JavascriptManagedKind::Object,
499 ))
500 .unwrap();
501 let subclass = class(&cx, 8, "DomainError");
502 realm.register_managed_class(subclassed_error, subclass.clone());
503
504 for (at, value) in [
505 JavascriptValue::Number(1.0),
506 JavascriptValue::Undefined,
507 JavascriptValue::Managed(plain),
508 ]
509 .into_iter()
510 .enumerate()
511 {
512 let raised = realm.raise(&cx, value.clone(), origin(at)).unwrap();
513 assert_eq!(realm.thrown_value(&raised), Some(&value));
514 }
515 let raised = realm
516 .raise(&cx, JavascriptValue::Managed(subclassed_error), origin(3))
517 .unwrap();
518 assert_eq!(raised.class_ref(), &subclass);
519 }
520
521 #[test]
522 fn checkpoint_drains_reentrant_microtasks_and_isolates_finalization() {
523 let trace = Rc::new(RefCell::new(Vec::new()));
524 let mut jobs = JavascriptJobs::new(AdmissionLimit(8));
525 let outer_trace = Rc::clone(&trace);
526 jobs.enqueue_microtask(move |queues| {
527 outer_trace.borrow_mut().push("reaction-1");
528 let inner_trace = Rc::clone(&outer_trace);
529 queues
530 .enqueue(
531 RuntimeJobClass::LanguageMicrotask("javascript"),
532 move |_| {
533 inner_trace.borrow_mut().push("reaction-2");
534 },
535 )
536 .unwrap();
537 })
538 .unwrap();
539 let final_trace = Rc::clone(&trace);
540 jobs.enqueue_finalization(move |_| final_trace.borrow_mut().push("finalize"))
541 .unwrap();
542 let receipt = jobs.microtask_checkpoint(WorkLimit(3)).unwrap();
543 assert_eq!(receipt.completed.len(), 2);
544 assert_eq!(&*trace.borrow(), &["reaction-1", "reaction-2"]);
545 jobs.finalization_checkpoint(WorkLimit(1)).unwrap();
546 assert_eq!(&*trace.borrow(), &["reaction-1", "reaction-2", "finalize"]);
547 }
548
549 #[test]
550 fn exhaustion_and_cancellation_fail_closed_without_implicit_work() {
551 let ran = Rc::new(RefCell::new(0));
552 let mut jobs = JavascriptJobs::new(AdmissionLimit(2));
553 let first_ran = Rc::clone(&ran);
554 let first = jobs
555 .enqueue_microtask(move |_| *first_ran.borrow_mut() += 1)
556 .unwrap();
557 let second_ran = Rc::clone(&ran);
558 jobs.enqueue_microtask(move |_| *second_ran.borrow_mut() += 1)
559 .unwrap();
560 assert_eq!(*ran.borrow(), 0);
561 assert_eq!(
562 jobs.enqueue_microtask(|_| {}).unwrap_err(),
563 CheckpointError::AdmissionExhausted
564 );
565 jobs.cancel(first);
566 assert_eq!(
567 jobs.microtask_checkpoint(WorkLimit(1)).unwrap_err(),
568 CheckpointError::WorkExhausted
569 );
570 assert_eq!(*ran.borrow(), 0);
571 jobs.microtask_checkpoint(WorkLimit(1)).unwrap();
572 assert_eq!(*ran.borrow(), 1);
573 }
574}