From 5d171f37babaf99005a236e97954d9ebf1244dad Mon Sep 17 00:00:00 2001 From: Noah <33094578+coolreader18@users.noreply.github.com> Date: Fri, 5 Feb 2021 23:18:02 -0600 Subject: [PATCH] Rework js promises a lot --- wasm/lib/src/convert.rs | 2 +- wasm/lib/src/js_module.rs | 254 +++++++++++++++++++++++++++++--------- 2 files changed, 197 insertions(+), 59 deletions(-) diff --git a/wasm/lib/src/convert.rs b/wasm/lib/src/convert.rs index 7ce8b3ead..b07838fb9 100644 --- a/wasm/lib/src/convert.rs +++ b/wasm/lib/src/convert.rs @@ -137,7 +137,7 @@ pub fn py_to_js(vm: &VirtualMachine, py_obj: PyObjectRef) -> JsValue { // the browser module might not be injected if vm.try_class("_js", "Promise").is_ok() { if let Some(py_prom) = py_obj.payload::() { - return py_prom.value().into(); + return py_prom.as_js(vm).into(); } } diff --git a/wasm/lib/src/js_module.rs b/wasm/lib/src/js_module.rs index ecc16524d..1f890b5f3 100644 --- a/wasm/lib/src/js_module.rs +++ b/wasm/lib/src/js_module.rs @@ -8,11 +8,12 @@ use wasm_bindgen_futures::{future_to_promise, JsFuture}; use rustpython_vm::builtins::{PyFloatRef, PyStrRef, PyTypeRef}; use rustpython_vm::exceptions::PyBaseExceptionRef; -use rustpython_vm::function::{Args, OptionalArg}; +use rustpython_vm::function::{Args, OptionalArg, OptionalOption}; use rustpython_vm::pyobject::{ BorrowValue, IntoPyObject, PyCallable, PyClassImpl, PyObjectRef, PyRef, PyResult, PyValue, StaticType, TryFromObject, }; +use rustpython_vm::slots::PyIter; use rustpython_vm::types::create_simple_type; use rustpython_vm::VirtualMachine; @@ -369,13 +370,21 @@ impl JsClosure { } } -#[pyclass(module = "browser", name = "Promise")] -#[derive(Debug)] +#[pyclass(module = "_js", name = "Promise")] +#[derive(Debug, Clone)] pub struct PyPromise { - value: Promise, + value: PromiseKind, } pub type PyPromiseRef = PyRef; +#[derive(Debug, Clone)] +enum PromiseKind { + Js(Promise), + PyProm { then: PyObjectRef }, + PyResolved(PyObjectRef), + PyRejected(PyBaseExceptionRef), +} + impl PyValue for PyPromise { fn class(_vm: &VirtualMachine) -> &PyTypeRef { Self::static_type() @@ -385,7 +394,9 @@ impl PyValue for PyPromise { #[pyimpl] impl PyPromise { pub fn new(value: Promise) -> PyPromise { - PyPromise { value } + PyPromise { + value: PromiseKind::Js(value), + } } pub fn from_future(future: F) -> PyPromise where @@ -393,73 +404,198 @@ impl PyPromise { { PyPromise::new(future_to_promise(future)) } - pub fn value(&self) -> Promise { - self.value.clone() + pub fn as_js(&self, vm: &VirtualMachine) -> Promise { + match &self.value { + PromiseKind::Js(prom) => prom.clone(), + PromiseKind::PyProm { then } => Promise::new(&mut |js_resolve, js_reject| { + let resolve = move |res: PyObjectRef, vm: &VirtualMachine| { + let _ = js_resolve.call1(&JsValue::UNDEFINED, &convert::py_to_js(vm, res)); + }; + let reject = move |err: PyBaseExceptionRef, vm: &VirtualMachine| { + let _ = + js_reject.call1(&JsValue::UNDEFINED, &convert::py_err_to_js_err(vm, &err)); + }; + let _ = vm.invoke( + then, + ( + vm.ctx.new_function("resolve", resolve), + vm.ctx.new_function("reject", reject), + ), + ); + }), + PromiseKind::PyResolved(obj) => Promise::resolve(&convert::py_to_js(vm, obj.clone())), + PromiseKind::PyRejected(err) => Promise::reject(&convert::py_err_to_js_err(vm, err)), + } + } + + fn cast(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let then = vm.get_attribute_opt(obj.clone(), "then")?; + let value = if let Some(then) = then.filter(|obj| vm.is_callable(obj)) { + PromiseKind::PyProm { then } + } else { + PromiseKind::PyResolved(obj) + }; + Ok(Self { value }) + } + + fn cast_result(res: PyResult, vm: &VirtualMachine) -> PyResult { + match res { + Ok(res) => Self::cast(res, vm), + Err(e) => Ok(Self { + value: PromiseKind::PyRejected(e), + }), + } + } + + #[pyclassmethod] + fn resolve(cls: PyTypeRef, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult> { + Self::cast(obj, vm)?.into_ref_with_type(vm, cls) + } + + #[pyclassmethod] + fn reject( + cls: PyTypeRef, + err: PyBaseExceptionRef, + vm: &VirtualMachine, + ) -> PyResult> { + Self { + value: PromiseKind::PyRejected(err), + } + .into_ref_with_type(vm, cls) } #[pymethod] fn then( &self, - on_fulfill: PyCallable, - on_reject: OptionalArg, + on_fulfill: OptionalOption, + on_reject: OptionalOption, vm: &VirtualMachine, - ) -> PyPromiseRef { - let weak_vm = weak_vm(vm); - let prom = JsFuture::from(self.value.clone()); + ) -> PyResult { + let (on_fulfill, on_reject) = (on_fulfill.flatten(), on_reject.flatten()); + if on_fulfill.is_none() && on_reject.is_none() { + return Ok(self.clone()); + } + match &self.value { + PromiseKind::Js(prom) => { + let weak_vm = weak_vm(vm); + let prom = JsFuture::from(prom.clone()); - let ret_future = async move { - let stored_vm = &weak_vm - .upgrade() - .expect("that the vm is valid when the promise resolves"); - let res = prom.await; - match res { - Ok(val) => stored_vm.interp.enter(move |vm| { - let args = if val.is_null() { - vec![] - } else { - vec![convert::js_to_py(vm, val)] - }; - let res = vm.invoke(&on_fulfill.into_object(), args); - convert::pyresult_to_jsresult(vm, res) - }), - Err(err) => { - if let OptionalArg::Present(on_reject) = on_reject { - stored_vm.interp.enter(move |vm| { - let err = convert::js_to_py(vm, err); - let res = vm.invoke(&on_reject.into_object(), (err,)); - convert::pyresult_to_jsresult(vm, res) - }) - } else { - Err(err) + let ret_future = async move { + let stored_vm = &weak_vm + .upgrade() + .expect("that the vm is valid when the promise resolves"); + let res = prom.await; + match res { + Ok(val) => match on_fulfill { + Some(on_fulfill) => stored_vm.interp.enter(move |vm| { + let val = convert::js_to_py(vm, val); + let res = on_fulfill.invoke((val,), vm); + convert::pyresult_to_jsresult(vm, res) + }), + None => Ok(val), + }, + Err(err) => match on_reject { + Some(on_reject) => stored_vm.interp.enter(move |vm| { + let err = convert::js_to_py(vm, err); + let res = on_reject.invoke((err,), vm); + convert::pyresult_to_jsresult(vm, res) + }), + None => Err(err), + }, } - } - } - }; + }; - PyPromise::from_future(ret_future).into_ref(vm) + Ok(PyPromise::from_future(ret_future)) + } + PromiseKind::PyProm { then } => { + Self::cast_result(vm.invoke(then, (on_fulfill, on_reject)), vm) + } + PromiseKind::PyResolved(res) => match on_fulfill { + Some(resolve) => Self::cast_result(resolve.invoke((res.clone(),), vm), vm), + None => Ok(self.clone()), + }, + PromiseKind::PyRejected(err) => match on_reject { + Some(reject) => Self::cast_result(reject.invoke((err.clone(),), vm), vm), + None => Ok(self.clone()), + }, + } } #[pymethod] - fn catch(&self, on_reject: PyCallable, vm: &VirtualMachine) -> PyPromiseRef { - let weak_vm = weak_vm(vm); - let prom = JsFuture::from(self.value.clone()); + fn catch( + &self, + on_reject: OptionalOption, + vm: &VirtualMachine, + ) -> PyResult { + self.then(OptionalArg::Present(None), on_reject, vm) + } - let ret_future = async move { - let err = match prom.await { - Ok(x) => return Ok(x), - Err(e) => e, - }; - let stored_vm = weak_vm - .upgrade() - .expect("that the vm is valid when the promise resolves"); - stored_vm.interp.enter(move |vm| { - let err = convert::js_to_py(vm, err); - let res = vm.invoke(&on_reject.into_object(), (err,)); - convert::pyresult_to_jsresult(vm, res) - }) - }; + #[pymethod(name = "__await__")] + fn r#await(zelf: PyRef) -> AwaitPromise { + AwaitPromise { + obj: Some(zelf.into_object()).into(), + } + } +} - PyPromise::from_future(ret_future).into_ref(vm) +#[pyclass(module = "_js", name = "AwaitPromise")] +struct AwaitPromise { + obj: cell::Cell>, +} + +impl fmt::Debug for AwaitPromise { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AwaitPromise").finish() + } +} + +impl PyValue for AwaitPromise { + fn class(_vm: &VirtualMachine) -> &PyTypeRef { + Self::static_type() + } +} + +#[pyimpl(with(PyIter))] +impl AwaitPromise { + #[pymethod] + fn send(&self, val: Option, vm: &VirtualMachine) -> PyResult { + match self.obj.take() { + Some(prom) => { + if val.is_some() { + Err(vm + .new_type_error("can't send non-None value to an awaitpromise".to_owned())) + } else { + Ok(prom) + } + } + None => Err(rustpython_vm::iterator::stop_iter_with_value( + vm.unwrap_or_none(val), + vm, + )), + } + } + + #[pymethod] + fn throw( + &self, + exc_type: PyObjectRef, + exc_val: OptionalArg, + exc_tb: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + let err = rustpython_vm::exceptions::normalize( + exc_type, + exc_val.unwrap_or_none(vm), + exc_tb.unwrap_or_none(vm), + vm, + )?; + Err(err) + } +} + +impl PyIter for AwaitPromise { + fn next(zelf: &PyRef, vm: &VirtualMachine) -> PyResult { + zelf.send(None, vm) } } @@ -478,6 +614,8 @@ pub fn make_module(vm: &VirtualMachine) -> PyObjectRef { "value" => ctx.new_readonly_getset("value", |exc: PyBaseExceptionRef| exc.get_arg(0)), }); + AwaitPromise::make_class(ctx); + py_module!(vm, "_js", { "JSError" => js_error, "JSValue" => PyJsValue::make_class(ctx),