use crate::fuel::Fuel;
use crate::heap::Heap;
use crate::outcome::{EvalResult, Outcome};
use crate::value::{PromiseId, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PromiseState {
Pending(Vec<PromiseHandler>),
Resolved(Value),
Rejected(Value),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PromiseHandler {
on_resolve: Option<Value>,
on_reject: Option<Value>,
chained: PromiseId,
}
impl PromiseHandler {
#[must_use]
pub fn new(on_resolve: Option<Value>, on_reject: Option<Value>, chained: PromiseId) -> Self {
Self {
on_resolve,
on_reject,
chained,
}
}
#[must_use]
pub fn on_resolve(&self) -> Option<&Value> {
self.on_resolve.as_ref()
}
#[must_use]
pub fn on_reject(&self) -> Option<&Value> {
self.on_reject.as_ref()
}
#[must_use]
pub fn chained(&self) -> PromiseId {
self.chained
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn then_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
match promise_id_of(&this) {
Some(promise_id) => {
let on_resolve = args.first().cloned();
let on_reject = args.get(1).cloned();
settle(promise_id, on_resolve, on_reject, heap, fuel)
}
None => Ok((
Outcome::Throw(type_error("Promise.prototype.then called on non-promise")),
heap,
fuel,
)),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn catch_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
match promise_id_of(&this) {
Some(promise_id) => {
let on_reject = args.first().cloned();
settle(promise_id, None, on_reject, heap, fuel)
}
None => Ok((
Outcome::Throw(type_error("Promise.prototype.catch called on non-promise")),
heap,
fuel,
)),
}
}
pub fn resolve(promise_id: PromiseId, value: Value, heap: Heap, fuel: Fuel) -> EvalResult {
settle_pending(
promise_id, value, true, heap, fuel,
)
}
pub fn reject(promise_id: PromiseId, value: Value, heap: Heap, fuel: Fuel) -> EvalResult {
settle_pending(
promise_id, value, false, heap, fuel,
)
}
fn settle_pending(
promise_id: PromiseId,
value: Value,
was_resolved: bool,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
match heap.promise(promise_id).cloned() {
Some(PromiseState::Pending(handlers)) => {
let new_state = if was_resolved {
PromiseState::Resolved(value.clone())
} else {
PromiseState::Rejected(value.clone())
};
let heap = heap
.store_promise(promise_id, new_state)
.unwrap_or_else(|h| h);
drain_handlers(handlers, value, was_resolved, heap, fuel)
}
Some(PromiseState::Resolved(_) | PromiseState::Rejected(_)) | None => {
Ok((Outcome::Normal(Value::Undefined), heap, fuel))
}
}
}
fn drain_handlers(
handlers: Vec<PromiseHandler>,
value: Value,
was_resolved: bool,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
handlers.into_iter().try_fold(
(Outcome::Normal(Value::Undefined), heap, fuel),
|(_, heap, fuel), handler| {
fire_one_handler(handler, value.clone(), was_resolved, heap, fuel)
},
)
}
fn fire_one_handler(
handler: PromiseHandler,
value: Value,
was_resolved: bool,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
let callback = if was_resolved {
handler.on_resolve().cloned()
} else {
handler.on_reject().cloned()
};
let chained = handler.chained();
match callback {
Some(cb) if is_callable(&cb) => {
crate::expression::call_function(&cb, &Value::Undefined, vec![value], heap, fuel)
.and_then(|(outcome, heap, fuel)| match outcome {
Outcome::Normal(result) => resolve(chained, result, heap, fuel),
Outcome::Throw(thrown) => reject(chained, thrown, heap, fuel),
})
}
Some(_) | None => {
if was_resolved {
resolve(chained, value, heap, fuel)
} else {
reject(chained, value, heap, fuel)
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn resolve_test_hook(args: Vec<Value>, _this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let promise_id = args.first().and_then(promise_id_of);
let value = args.get(1).cloned().unwrap_or(Value::Undefined);
match promise_id {
Some(id) => resolve(id, value, heap, fuel),
None => Ok((Outcome::Normal(Value::Undefined), heap, fuel)),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn reject_test_hook(args: Vec<Value>, _this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let promise_id = args.first().and_then(promise_id_of);
let value = args.get(1).cloned().unwrap_or(Value::Undefined);
match promise_id {
Some(id) => reject(id, value, heap, fuel),
None => Ok((Outcome::Normal(Value::Undefined), heap, fuel)),
}
}
#[must_use]
pub fn install_test_hooks(env: crate::env::Env, heap: Heap) -> (crate::env::Env, Heap) {
let (resolve_cell, heap) = heap.alloc_cell(crate::value::Cell::new(
Value::Native(resolve_test_hook),
false,
));
let (reject_cell, heap) = heap.alloc_cell(crate::value::Cell::new(
Value::Native(reject_test_hook),
false,
));
let env = env
.extend_cell("__resolve_promise", resolve_cell)
.extend_cell("__reject_promise", reject_cell);
(env, heap)
}
fn promise_id_of(value: &Value) -> Option<PromiseId> {
match value {
Value::Promise(id) => Some(*id),
Value::Undefined
| Value::Null
| Value::Boolean(_)
| Value::Number(_)
| Value::String(_)
| Value::Object(_)
| Value::Function(_)
| Value::Native(_) => None,
}
}
fn settle(
promise_id: PromiseId,
on_resolve: Option<Value>,
on_reject: Option<Value>,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
match heap.promise(promise_id).cloned() {
Some(PromiseState::Resolved(value)) => fire_resolve_handler(value, on_resolve, heap, fuel),
Some(PromiseState::Rejected(value)) => fire_reject_handler(value, on_reject, heap, fuel),
Some(PromiseState::Pending(handlers)) => {
queue_handler(promise_id, handlers, on_resolve, on_reject, heap, fuel)
}
None => Ok((
Outcome::Throw(type_error("promise missing from heap")),
heap,
fuel,
)),
}
}
fn fire_resolve_handler(
value: Value,
on_resolve: Option<Value>,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
match on_resolve {
Some(callback) if is_callable(&callback) => {
crate::expression::call_function(&callback, &Value::Undefined, vec![value], heap, fuel)
.and_then(|(outcome, heap, fuel)| chain_outcome(outcome, heap, fuel))
}
Some(_) | None => alloc_resolved_value(value, heap, fuel),
}
}
fn fire_reject_handler(
value: Value,
on_reject: Option<Value>,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
match on_reject {
Some(callback) if is_callable(&callback) => {
crate::expression::call_function(&callback, &Value::Undefined, vec![value], heap, fuel)
.and_then(|(outcome, heap, fuel)| chain_outcome(outcome, heap, fuel))
}
Some(_) | None => alloc_rejected_value(value, heap, fuel),
}
}
#[allow(clippy::unnecessary_wraps)] fn queue_handler(
source: PromiseId,
existing_handlers: Vec<PromiseHandler>,
on_resolve: Option<Value>,
on_reject: Option<Value>,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
let (chained, heap) = heap.alloc_promise(PromiseState::Pending(Vec::new()));
let handler = PromiseHandler::new(on_resolve, on_reject, chained);
let extended_handlers: Vec<PromiseHandler> = existing_handlers
.into_iter()
.chain(std::iter::once(handler))
.collect();
let heap = heap
.store_promise(source, PromiseState::Pending(extended_handlers))
.unwrap_or_else(|h| h);
Ok((Outcome::Normal(Value::Promise(chained)), heap, fuel))
}
fn chain_outcome(outcome: Outcome, heap: Heap, fuel: Fuel) -> EvalResult {
match outcome {
Outcome::Normal(value) => alloc_resolved_value(value, heap, fuel),
Outcome::Throw(value) => alloc_rejected_value(value, heap, fuel),
}
}
#[allow(clippy::unnecessary_wraps)] fn alloc_resolved_value(value: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let (id, heap) = heap.alloc_promise(PromiseState::Resolved(value));
Ok((Outcome::Normal(Value::Promise(id)), heap, fuel))
}
#[allow(clippy::unnecessary_wraps)] fn alloc_rejected_value(value: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let (id, heap) = heap.alloc_promise(PromiseState::Rejected(value));
Ok((Outcome::Normal(Value::Promise(id)), heap, fuel))
}
fn is_callable(value: &Value) -> bool {
matches!(value, Value::Function(_) | Value::Native(_))
}
fn type_error(message: &str) -> Value {
Value::String(format!("TypeError: {message}"))
}