Skip to content
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

Support components result types #391

Merged
merged 3 commits into from
Nov 3, 2024
Merged

Support components result types #391

merged 3 commits into from
Nov 3, 2024

Conversation

jbourassa
Copy link
Collaborator

Add support for the component model's result type.

result<O, E> are mapped to instances of Wasmtime::Component::Result. I intentionally left the interface simple, but we can expand it if we see fit.

Some design choices:

  • Result behave like value objects for equality and hashing, similar to hash, array, etc.
  • Result is implemented in Ruby for simplicty.
  • When converting Ruby Result to a result branch of a none type (e.g. unparametrized result), the Ruby value must be nil. This follows the same spirit that the Ruby bindings generally avoids type casting as a way to avoid subtle bugs.
    Example: converting Result.ok(1) to a component model result<_, E> will error. Note that _ here means "unit".

This PR also includes slight non-controversial tweaks to tests and conversion logic of records.

Comment on lines +184 to +222
Type::Result(result_type) => {
// Expect value to conform to `Wasmtime::Component::Value`'s interface
let ruby = Ruby::get().unwrap();
let is_ok = value.funcall::<_, (), bool>(IS_OK.into_id_with(&ruby), ())?;

if is_ok {
let ok_value = value.funcall::<_, (), Value>(OK.into_id_with(&ruby), ())?;
match result_type.ok() {
Some(ty) => rb_to_component_val(ok_value, _store, &ty)
.map(|val| Val::Result(Result::Ok(Some(Box::new(val))))),
None => {
if ok_value.is_nil() {
Ok(Val::Result(Ok(None)))
} else {
err!(
"expected nil for result<_, E> ok branch, got {}",
ok_value.inspect()
)
}
}
}
} else {
let err_value = value.funcall::<_, (), Value>(ERROR.into_id_with(&ruby), ())?;
match result_type.err() {
Some(ty) => rb_to_component_val(err_value, _store, &ty)
.map(|val| Val::Result(Result::Err(Some(Box::new(val))))),
None => {
if err_value.is_nil() {
Ok(Val::Result(Err(None)))
} else {
err!(
"expected nil for result<O, _> error branch, got {}",
err_value.inspect()
)
}
}
}
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a mouthful. I tried simplifying but did not come up with anything much better. Ideas are welcome.

@jbourassa jbourassa marked this pull request as ready for review October 25, 2024 17:16
Copy link
Member

@saulecabrera saulecabrera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When converting Ruby Result to a result branch of a none type (e.g. unparametrized result), the Ruby value must be nil. This follows the same spirit that the Ruby bindings generally avoids type casting as a way to avoid subtle bugs.
Example: converting Result.ok(1) to a component model result<_, E> will error. Note that _ here means "unit".

IMHO, this is an important design decision that should probably be documented somewhere in the public API/docs? Maybe the result class is a good place to start?

Comment on lines +195 to +204
if ok_value.is_nil() {
Ok(Val::Result(Ok(None)))
} else {
err!(
"expected nil for result<_, E> ok branch, got {}",
ok_value.inspect()
)
}
}
}
Copy link
Member

@saulecabrera saulecabrera Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This invariant is the same-ish in both cases (ok, error), perhaps this could be extracted into a local function?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this more or less what you had in mind?

@@ -185,38 +198,40 @@ pub(crate) fn rb_to_component_val(
             // Expect value to conform to `Wasmtime::Component::Value`'s interface
             let ruby = Ruby::get().unwrap();
             let is_ok = value.funcall::<_, (), bool>(IS_OK.into_id_with(&ruby), ())?;
+            let expect_nil = |value: Value,
+                              nil_case: Val,
+                              error_handler: fn(Value) -> Result<Val, Error>|
+             -> Result<Val, Error> {
+                if value.is_nil() {
+                    Ok(nil_case)
+                } else {
+                    error_handler(value)
+                }
+            };
 
             if is_ok {
                 let ok_value = value.funcall::<_, (), Value>(OK.into_id_with(&ruby), ())?;
                 match result_type.ok() {
                     Some(ty) => rb_to_component_val(ok_value, _store, &ty)
                         .map(|val| Val::Result(Result::Ok(Some(Box::new(val))))),
-                    None => {
-                        if ok_value.is_nil() {
-                            Ok(Val::Result(Ok(None)))
-                        } else {
-                            err!(
-                                "expected nil for result<_, E> ok branch, got {}",
-                                ok_value.inspect()
-                            )
-                        }
-                    }
+                    None => expect_nil(ok_value, Val::Result(Ok(None)), |v| {
+                        err!(
+                            "expected nil for result<_, E> ok branch, got {}",
+                            v.inspect()
+                        )
+                    }),
                 }
             } else {
                 let err_value = value.funcall::<_, (), Value>(ERROR.into_id_with(&ruby), ())?;
                 match result_type.err() {
                     Some(ty) => rb_to_component_val(err_value, _store, &ty)
                         .map(|val| Val::Result(Result::Err(Some(Box::new(val))))),
-                    None => {
-                        if err_value.is_nil() {
-                            Ok(Val::Result(Err(None)))
-                        } else {
-                            err!(
-                                "expected nil for result<O, _> error branch, got {}",
-                                err_value.inspect()
-                            )
-                        }
-                    }
+                    None => expect_nil(err_value, Val::Result(Err(None)), |v| {
+                        err!(
+                            "expected nil for result<O, _> error branch, got {}",
+                            v.inspect()
+                        )
+                    }),
                 }
             }
         }

I preferred the prior version; this one doesn't communicate the intent much better but adds indirection. If you can think of a better approach, please share 😄.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good keeping the previous version!

@jbourassa
Copy link
Collaborator Author

IMHO, this is an important design decision that should probably be documented somewhere in the public API/docs? Maybe the result class is a good place to start?

Agreed, this decision should be documented.

A doc section on component model <-> Ruby type conversion is necessary. I think the most natural place for it to go is in the API doc for Instance#invoke / Func#call (to be implemented) API doc. In fact, I was thinking of removing Instance#invoke altogether to match the rust crate API.

I'll make those 2 changes (introduce Func + conversion doc) in its own PR, outside of this one.

*  Do not remap Hash conversion error to Wasmtime::Error,
   for consistency with other convert error types.
*  Treat missing fields as such, instead of converting to nil.
   This will lead to better error messages.
@jbourassa jbourassa merged commit 6b07e2b into main Nov 3, 2024
23 checks passed
@jbourassa jbourassa deleted the components-result branch November 17, 2024 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants