Skip to content

Async #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
982925e
feat: implement basic async
WhaleKit Jan 1, 2025
ff0c467
test: add example/test for async
WhaleKit Jan 1, 2025
30292a6
remove unnecessary mut for making exported memory
WhaleKit Jan 5, 2025
9ff3d6d
fix: support for multiple resumes on single suspendedFucn now works
WhaleKit Jan 5, 2025
b66cd50
feat: add support for suspending execution by timer, atomic flag or u…
WhaleKit Jan 5, 2025
250402c
ci: add test for suspending and resuming wasm code
WhaleKit Jan 5, 2025
3aa69da
chore: fix codestyle in host_coro example
WhaleKit Jan 5, 2025
d59d31f
fix bug with br_if instruction corrupting stack
WhaleKit Jan 8, 2025
9454509
Merge branch 'next' into async
WhaleKit Jan 8, 2025
a0bfa66
make wasm_resume nominally look like a test
WhaleKit Jan 8, 2025
9a90550
fix: building with no-std
WhaleKit Jan 8, 2025
b49c1c4
support suspending start function when instantiating module
WhaleKit Jan 9, 2025
9b7496b
ci: add feature to run test-suite with async suspends
WhaleKit Jan 9, 2025
89d9dcc
minor: clippy, names, qol function
WhaleKit Jan 9, 2025
ff933a2
chore: reorganized some code
WhaleKit Jan 11, 2025
b73ab18
codestyle, relax bounds on YieldedValue/ResumeArgument
WhaleKit Jan 11, 2025
ae83085
feat: added call_coro to typed fucn handle, some code reorganization …
WhaleKit Jan 11, 2025
a41f68b
fix: fix no-std build
WhaleKit Jan 11, 2025
27c7d66
move resume test
WhaleKit Jan 12, 2025
450881e
feat: all async/resume functionality is now feature-gated
WhaleKit Jan 13, 2025
cf85b34
fix various issues
WhaleKit Jan 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ rust-version.workspace=true
name="wasm-rust"
test=false

[[example]]
name="host_coro"
required-features=["async"]

[dev-dependencies]
wat={workspace=true}
eyre={workspace=true}
Expand Down
2 changes: 2 additions & 0 deletions crates/tinywasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ logging=["log", "tinywasm-parser?/logging", "tinywasm-types/logging"]
std=["tinywasm-parser?/std", "tinywasm-types/std"]
parser=["dep:tinywasm-parser"]
archive=["tinywasm-types/archive"]
async=[]
test_async=["async"] #feels weird putting it here

[[test]]
name="test-wasm-1"
Expand Down
260 changes: 260 additions & 0 deletions crates/tinywasm/src/coro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
#![cfg_attr(not(feature = "async"), allow(unused))]
#![cfg_attr(not(feature = "async"), allow(unreachable_pub))]

mod module {
use crate::Result;
use core::fmt::Debug;
pub(crate) use tinywasm_types::{ResumeArgument, YieldedValue};

///"coroutine statse", "coroutine instance", "resumable". Stores info to continue a function that was paused
pub trait CoroState<Ret, ResumeContext>: Debug {
#[cfg(feature = "async")]
/// resumes the execution of the coroutine
fn resume(&mut self, ctx: ResumeContext, arg: ResumeArgument) -> Result<CoroStateResumeResult<Ret>>;
}

/// explains why did execution suspend, and carries payload if needed
#[derive(Debug)]
#[non_exhaustive] // some variants are feature-gated
#[cfg(feature = "async")]
pub enum SuspendReason {
/// host function yielded
/// some host functions might expect resume argument when calling resume
Yield(YieldedValue),

/// time to suspend has come,
/// host shouldn't provide resume argument when calling resume
#[cfg(feature = "std")]
SuspendedEpoch,

/// user's should-suspend-callback returned Break,
/// host shouldn't provide resume argument when calling resume
SuspendedCallback,

/// async should_suspend flag was set
/// host shouldn't provide resume argument when calling resume
SuspendedFlag,
// possible others: delimited continuations proposal, debugger breakpoint, out of fuel
}

#[cfg(not(feature = "async"))]
pub type SuspendReason = core::convert::Infallible;

/// result of a function that might pause in the middle and yield
/// to be resumed later
#[derive(Debug)]
pub enum PotentialCoroCallResult<R, State>
//where for<Ctx>
// State: CoroState<R, Ctx>, // can't in stable rust
{
/// function returns normally
Return(R),
/// interpreter will be suspended and execution will return to host along with SuspendReason
Suspended(SuspendReason, State),
}

/// result of resuming coroutine state. Unlike [`PotentialCoroCallResult`]
/// doesn't need to have state, since it's contained in self
#[derive(Debug)]
pub enum CoroStateResumeResult<R> {
/// CoroState has finished
/// after this CoroState::resume can't be called again on that CoroState
Return(R),

/// host function yielded
/// execution returns to host along with yielded value
Suspended(SuspendReason),
}

impl<R, State> PotentialCoroCallResult<R, State> {
/// in case you expect function only to return
/// you can make Suspend into [crate::Error::UnexpectedSuspend] error
pub fn suspend_to_err(self) -> Result<R> {
match self {
PotentialCoroCallResult::Return(r) => Ok(r),
#[cfg(feature = "async")]
PotentialCoroCallResult::Suspended(r, _) => Err(crate::Error::UnexpectedSuspend(r.into())),
}
}

/// true if coro is finished
pub fn finished(&self) -> bool {
matches!(self, Self::Return(_))
}
/// separates state from PotentialCoroCallResult, leaving CoroStateResumeResult (one without state)
pub fn split_state(self) -> (CoroStateResumeResult<R>, Option<State>) {
match self {
Self::Return(val) => (CoroStateResumeResult::Return(val), None),
Self::Suspended(suspend, state) => (CoroStateResumeResult::Suspended(suspend), Some(state)),
}
}
/// separates result from PotentialCoroCallResult, leaving unit type in it's place
pub fn split_result(self) -> (PotentialCoroCallResult<(), State>, Option<R>) {
match self {
Self::Return(result) => (PotentialCoroCallResult::Return(()), Some(result)),
Self::Suspended(suspend, state) => (PotentialCoroCallResult::Suspended(suspend, state), None),
}
}

/// transforms state
pub fn map_state<OutS>(self, mapper: impl FnOnce(State) -> OutS) -> PotentialCoroCallResult<R, OutS> {
match self {
Self::Return(val) => PotentialCoroCallResult::Return(val),
Self::Suspended(suspend, state) => PotentialCoroCallResult::Suspended(suspend, mapper(state)),
}
}
/// transform result with mapper if there is none - calls "otherwise".
/// user_val passed to whichever is called and is guaranteed to be used
pub fn map<OutR, Usr, OutS>(
self,
user_val: Usr,
res_mapper: impl FnOnce(R, Usr) -> OutR,
state_mapper: impl FnOnce(State, Usr) -> OutS,
) -> PotentialCoroCallResult<OutR, OutS> {
match self {
Self::Return(res) => PotentialCoroCallResult::Return(res_mapper(res, user_val)),
Self::Suspended(suspend, state) => {
PotentialCoroCallResult::Suspended(suspend, state_mapper(state, user_val))
}
}
}
/// transforms result
pub fn map_result<OutR>(self, mapper: impl FnOnce(R) -> OutR) -> PotentialCoroCallResult<OutR, State> {
self.map((), |val, _| mapper(val), |s, _| s)
}
}

impl<R, State, E> PotentialCoroCallResult<core::result::Result<R, E>, State> {
/// turns Self<Result<R>, S> into Resulf<Self<R>, S>
pub fn propagate_err_result(self) -> core::result::Result<PotentialCoroCallResult<R, State>, E> {
Ok(match self {
PotentialCoroCallResult::Return(res) => PotentialCoroCallResult::<R, State>::Return(res?),
PotentialCoroCallResult::Suspended(why, state) => {
PotentialCoroCallResult::<R, State>::Suspended(why, state)
}
})
}
}
impl<R, State, E> PotentialCoroCallResult<R, core::result::Result<State, E>> {
/// turns Self<R, Result<S>> into Resulf<R, Self<S>>
pub fn propagate_err_state(self) -> core::result::Result<PotentialCoroCallResult<R, State>, E> {
Ok(match self {
PotentialCoroCallResult::Return(res) => PotentialCoroCallResult::<R, State>::Return(res),
PotentialCoroCallResult::Suspended(why, state) => {
PotentialCoroCallResult::<R, State>::Suspended(why, state?)
}
})
}
}

impl<R> CoroStateResumeResult<R> {
/// in case you expect function only to return
/// you can make Suspend into [crate::Error::UnexpectedSuspend] error
pub fn suspend_to_err(self) -> Result<R> {
match self {
Self::Return(r) => Ok(r),
#[cfg(feature = "async")]
Self::Suspended(r) => Err(crate::Error::UnexpectedSuspend(r.into())),
}
}

/// true if coro is finished
pub fn finished(&self) -> bool {
matches!(self, Self::Return(_))
}
/// separates result from CoroStateResumeResult, leaving unit type in it's place
pub fn split_result(self) -> (CoroStateResumeResult<()>, Option<R>) {
let (a, r) = PotentialCoroCallResult::<R, ()>::from(self).split_result();
(a.into(), r)
}
/// transforms result
pub fn map_result<OutR>(self, mapper: impl FnOnce(R) -> OutR) -> CoroStateResumeResult<OutR> {
PotentialCoroCallResult::<R, ()>::from(self).map_result(mapper).into()
}
/// transform result with mapper. If there is none - calls "otherwise"
/// user_val passed to whichever is called and is guaranteed to be used
pub fn map<OutR, Usr>(
self,
user_val: Usr,
mapper: impl FnOnce(R, Usr) -> OutR,
otherwise: impl FnOnce(Usr),
) -> CoroStateResumeResult<OutR> {
PotentialCoroCallResult::<R, ()>::from(self).map(user_val, mapper, |(), usr| otherwise(usr)).into()
}
}

impl<R, E> CoroStateResumeResult<core::result::Result<R, E>> {
/// turns Self<Result<R>> into Resulf<Self<R>>
pub fn propagate_err(self) -> core::result::Result<CoroStateResumeResult<R>, E> {
Ok(PotentialCoroCallResult::<core::result::Result<R, E>, ()>::from(self).propagate_err_result()?.into())
}
}

// convert between PotentialCoroCallResult<SrcR, ()> and CoroStateResumeResult<SrcR>
impl<DstR, SrcR> From<PotentialCoroCallResult<SrcR, ()>> for CoroStateResumeResult<DstR>
where
DstR: From<SrcR>,
{
fn from(value: PotentialCoroCallResult<SrcR, ()>) -> Self {
match value {
PotentialCoroCallResult::Return(val) => Self::Return(val.into()),
PotentialCoroCallResult::Suspended(suspend, ()) => Self::Suspended(suspend),
}
}
}
impl<SrcR> From<CoroStateResumeResult<SrcR>> for PotentialCoroCallResult<SrcR, ()> {
fn from(value: CoroStateResumeResult<SrcR>) -> Self {
match value {
CoroStateResumeResult::Return(val) => PotentialCoroCallResult::Return(val),
CoroStateResumeResult::Suspended(suspend) => PotentialCoroCallResult::Suspended(suspend, ()),
}
}
}

#[cfg(feature = "async")]
impl SuspendReason {
/// shotrhand to package val into a Box<any> in a [SuspendReason::Yield] variant
/// you'll need to specify type explicitly, because you'll need to use exact same type when downcasting
pub fn make_yield<T>(val: impl Into<T> + core::any::Any) -> Self {
Self::Yield(Some(alloc::boxed::Box::new(val) as alloc::boxed::Box<dyn core::any::Any>))
}
}

/// for use in error [`crate::Error::UnexpectedSuspend`]
/// same as [SuspendReason], but without [tinywasm_types::YieldedValue], since we can't debug-print it
/// and including it would either require YieldedValue to be Send+Sync or disable that for Error
#[derive(Debug)]
pub enum UnexpectedSuspendError {
/// host function yielded
Yield,

/// timeout,
#[cfg(feature = "std")]
SuspendedEpoch,

/// user's should-suspend-callback returned Break,
SuspendedCallback,

/// async should_suspend flag was set
SuspendedFlag,
}

#[cfg(feature = "async")]
impl From<SuspendReason> for UnexpectedSuspendError {
fn from(value: SuspendReason) -> Self {
match value {
SuspendReason::Yield(_) => Self::Yield,
#[cfg(feature = "std")]
SuspendReason::SuspendedEpoch => Self::SuspendedEpoch,
SuspendReason::SuspendedCallback => Self::SuspendedCallback,
SuspendReason::SuspendedFlag => Self::SuspendedFlag,
}
}
}
}

#[cfg(feature = "async")]
pub use module::*;

#[cfg(not(feature = "async"))]
pub(crate) use module::*;
27 changes: 24 additions & 3 deletions crates/tinywasm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ use tinywasm_types::FuncType;
#[cfg(feature = "parser")]
pub use tinywasm_parser::ParseError;

#[cfg(feature = "async")]
use crate::coro::UnexpectedSuspendError;

use crate::interpreter;

/// Errors that can occur for `TinyWasm` operations
#[derive(Debug)]
pub enum Error {
Expand Down Expand Up @@ -35,6 +40,17 @@ pub enum Error {
/// The store is not the one that the module instance was instantiated in
InvalidStore,

/// ResumeArgument of wrong type was provided
InvalidResumeArgument,

/// Tried to resume on runtime when it's not suspended
InvalidResume,

/// Function unexpectedly yielded instead of returning
/// (for backwards compatibility with old api)
#[cfg(feature = "async")]
UnexpectedSuspend(UnexpectedSuspendError),

#[cfg(feature = "std")]
/// An I/O error occurred
Io(crate::std::io::Error),
Expand Down Expand Up @@ -184,6 +200,9 @@ impl Display for Error {
#[cfg(feature = "std")]
Self::Io(err) => write!(f, "I/O error: {err}"),

#[cfg(feature = "async")]
Self::UnexpectedSuspend(_) => write!(f, "funtion yielded instead of returning"),

Self::Trap(trap) => write!(f, "trap: {trap}"),
Self::Linker(err) => write!(f, "linking error: {err}"),
Self::InvalidLabelType => write!(f, "invalid label type"),
Expand All @@ -193,6 +212,8 @@ impl Display for Error {
write!(f, "invalid host function return: expected={expected:?}, actual={actual:?}")
}
Self::InvalidStore => write!(f, "invalid store"),
Self::InvalidResumeArgument => write!(f, "invalid resume argument supplied to suspended function"),
Self::InvalidResume => write!(f, "attempt to resume coroutine that has already finished"),
}
}
}
Expand Down Expand Up @@ -246,14 +267,14 @@ impl From<tinywasm_parser::ParseError> for Error {
pub type Result<T, E = Error> = crate::std::result::Result<T, E>;

pub(crate) trait Controlify<T> {
fn to_cf(self) -> ControlFlow<Option<Error>, T>;
fn to_cf(self) -> ControlFlow<interpreter::executor::ReasonToBreak, T>;
}

impl<T> Controlify<T> for Result<T, Error> {
fn to_cf(self) -> ControlFlow<Option<Error>, T> {
fn to_cf(self) -> ControlFlow<interpreter::executor::ReasonToBreak, T> {
match self {
Ok(value) => ControlFlow::Continue(value),
Err(err) => ControlFlow::Break(Some(err)),
Err(err) => ControlFlow::Break(interpreter::executor::ReasonToBreak::Errored(err)),
}
}
}
Loading
Loading