-
-
Notifications
You must be signed in to change notification settings - Fork 211
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
Make FromGodot
conversions fallible
#467
Make FromGodot
conversions fallible
#467
Conversation
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-467 |
585b828
to
5bd9984
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot!
ConvertError currently appears in both builtin::meta
and builtin
modules, but should only have two symbols in total (once in builtin::meta
, once in prelude
.
Not yet reviewed everything, but already submitting first half 🙂
Unrelated to this PR, but I'm not sure if we should nest modules inside godot::prelude
. It probably helps to keep some types shorter but still somewhat qualified? The others like prelude::{math, utilities, real_consts}
etc. make some sense, but prelude::meta
in particular is questionable. Its traits ToGodot
/etc should be part of prelude
itself, and other things like ClassName
or PropertyInfo
are so niche that they can just be fully qualified in their original module.
No need to change anything here (apart from the ConvertError
duplication), but I wanted to ask how you see this?
} | ||
} | ||
|
||
#[derive(Debug, PartialEq, Eq)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[derive(Debug, PartialEq, Eq)] | |
#[derive(Eq, PartialEq, Debug)] |
😛 (see here)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still open, but I can address it sometime else, not worth blocking this 😉
} | ||
|
||
#[derive(Debug, PartialEq, Eq)] | ||
#[non_exhaustive] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need #[non_exhaustive]
if the type is not public?
Inside the crate itself, we're not forced to write else
branches in match
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we shouldn't, it's leftover from when the enums were public
Self::BadType { expected, got } => { | ||
format!("expected Variant of type `{expected:?}` but got Variant of type `{got:?}`") | ||
} | ||
Self::WrongClass { expected } => { | ||
format!("got variant of wrong class, expected class `{expected}`") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would keep the "expected A, but got B" order consistent throughout error messages.
Also, first message uses Debug
and second Display
repr.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i can't use Display
for the first one without implementing Display
for VariantType
. and the implementation would likely be identical to the Debug
impl.
and i dont want a debug version of ClassName
printed, i just want the class name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, thanks for elaborating!
/// Variant value cannot be represented in target type | ||
BadValue, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably also contain the variant type for more context:
/// Variant value cannot be represented in target type | |
BadValue, | |
/// Variant value cannot be represented in target type | |
BadValue { | |
expected: VariantType, | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think i can actually just remove it, since it's only used in one place where BadType
is the more appropriate error
impl ConvertError { | ||
/// Create a new error for a conversion. | ||
fn new(kind: ErrorKind) -> Self { | ||
Self { | ||
kind, | ||
cause: None, | ||
value: None, | ||
} | ||
} | ||
|
||
/// Create a new custom error for a conversion. | ||
pub fn custom() -> Self { | ||
Self::new(ErrorKind::Custom) | ||
} | ||
|
||
/// Add a rust-error as an underlying cause for the conversion error. | ||
pub fn with_cause<C: Into<Cause>>(mut self, cause: C) -> Self { | ||
self.cause = Some(cause.into()); | ||
self | ||
} | ||
|
||
/// Returns the rust-error that caused this error, if one exists. | ||
pub fn cause(&self) -> Option<&(dyn Error + Send + Sync)> { | ||
self.cause.as_deref() | ||
} | ||
|
||
/// Add the value that failed to be converted. | ||
pub fn with_value<V: fmt::Debug + 'static>(mut self, value: V) -> Self { | ||
self.value = Some(Box::new(value)); | ||
self | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this "almost-builder" constructor design. You have a lot of patterns like this:
ConvertError::from(err).with_value(val)
which could be written more type-safely as:
ConvertError::with_value(err, val)
This would get rid of the From
conversion and would protect invariants (value is not forgotten).
From
feels wrong here, the resulting object is not equivalent but needs additional information to be useful.
How many possible combinations are we facing here? Now there are already 3-4 constructors/builders, I don't think there would be significantly more constructors if all cases are covered explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're mostly just there so people can quickly create custom errors if they want tbh, but i think i can make the internal methods more type safe
PtrcallReturnT::<$R>::call(|return_ptr| { | ||
utility_fn(return_ptr, type_ptrs.as_ptr(), type_ptrs.len() as i32); | ||
}) | ||
}).unwrap_or_else(|err| return_error::<Self::Ret>(method_name, err)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using a variable here (this pattern appears 4x in total).
@@ -205,13 +210,15 @@ macro_rules! impl_varcall_signature_for_tuple { | |||
|
|||
check_varcall_error(&err, method_name, &explicit_args, varargs); | |||
}); | |||
<Self::Ret as FromVariantIndirect>::convert(variant) | |||
|
|||
<Self::Ret as FromGodot>::try_from_variant(&variant).unwrap_or_else(|err| return_error::<Self::Ret>(method_name, err)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also here, a variable to store <Self::Ret as FromGodot>::try_from_variant(&variant)
might be more readable.
It would also help annotate intermediate expressions with types in IDEs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed the rest. Thanks a lot for all the tests! 👍
let root = variant.try_to::<::godot::builtin::Dictionary>().ok()?; | ||
let root = root.get(#name_string)?; | ||
let root = variant.try_to::<::godot::builtin::Dictionary>()?; | ||
let root = root.get(#name_string).ok_or(ConvertError::custom().with_cause(concat!("missing expected value ", #name_string)).with_value(root.clone()))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of concat!
in the generated code, the string could be concatenated outside of quote!
.
Also, splitting root.get(#name_string)
into a separate variable would allow to avoid the root.clone()
, no? Since get
returns a value and doesn't borrow root
any longer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure though i was planning on rewriting this whole derive macro soon anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, in that case no need to bother 🙂
@@ -128,20 +131,20 @@ fn make_named_struct( | |||
} else { | |||
( | |||
quote! { | |||
let #ident = root.get(#string_ident)?; | |||
let #ident = root.get(#string_ident).ok_or(ConvertError::custom().with_cause(concat!("missing expected value ", #string_ident)).with_value(root.clone()))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see above
let #ident = root.pop_front() | ||
.ok_or(ConvertError::custom().with_cause("missing expected value").with_value(root.clone()))? | ||
.try_to::<#field_type>()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would do indentation like regular rustfmt: from start of the scope, not aligned with the first .
let #ident = variant.pop_front() | ||
.ok_or(ConvertError::custom().with_cause("missing expected value").with_value(variant.clone()))? | ||
.try_to::<#field_type>()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see above
node.free(); | ||
} | ||
|
||
/// Check that the value stored in an error is the same as what was given. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"what was given", you mean passed to its constructor?
const MISSING_KEY_A: &'static str = "missing `a` key"; | ||
const MISSING_KEY_B: &'static str = "missing `a` key"; | ||
const TOO_MANY_KEYS: &'static str = "missing `a` key"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably forgot to update 2nd and 3rd literal 🙂
format!("{:?}", err.value().unwrap()), | ||
format!("{:?}", "hello".to_variant()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not obvious why you compare the Debug
reprs here -- is it because one can't fetch/downcast the original type anymore? Maybe add a comment 🤔
(also in other such cases)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh yeah i did add a comment earlier in another case but i can add it here too
2984e19
to
6ed8bd5
Compare
Rebased onto |
6ed8bd5
to
51ab4e0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly good, a few unresolved conversations (also from last time). Feel free to arrange/squash commits to your liking 🙂
godot-core/src/builtin/array.rs
Outdated
/// Creates a new reference to the data in this array. | ||
/// | ||
/// This does not ensure the Array's type matches what we statically expect. | ||
fn clone_unchecked(&self) -> Self { | ||
// SAFETY: `self` is a valid array, since we have a reference that keeps it alive. | ||
unsafe { | ||
Self::from_sys_init(|self_ptr| { | ||
let ctor = ::godot_ffi::builtin_fn!(array_construct_copy); | ||
let args = [self.sys_const()]; | ||
ctor(self_ptr, args.as_ptr()); | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this method need to be unsafe
itself, as the caller needs to make sure the invariants are upheld?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nvm turns out that changing this to no longer need this method was an easy fix
I dont think this actually needs to be a safety, it just needs to be a correctness invariant. As i don't believe anything in Array
actually relies on the values stored in the array to be T
, and anything that expects it to be will panic/error when it's not. If this actually should be unsafe then i'd need to rewrite some stuff. Maybe @ttencate knows for sure?
We can refer to the assume_type
method here, whose safety doc says:
/// In and of itself, calling this does not result in undefined behavior. However:
/// - If `T` is not `Variant`, the returned array should not be written to, because the runtime
/// type check may fail.
/// - If `U` is not `Variant`, the returned array should not be read from, because conversion
/// from variants may fail.
///
/// In the current implementation, both cases will produce a panic rather than undefined
/// behavior, but this should not be relied upon.
I guess not marking this as unsafe
would be a commitment to the current behavior, meaning assume_type
should be changed to be a safe function. Which wouldn't be the worst, we could in the future provide *_unchecked
methods as alternatives in cases where we currently panic?
godot-core/src/builtin/array.rs
Outdated
@@ -627,7 +651,8 @@ impl<T: GodotType> GodotConvert for Array<T> { | |||
|
|||
impl<T: GodotType> ToGodot for Array<T> { | |||
fn to_godot(&self) -> Self::Via { | |||
self.clone() | |||
// `to_godot` is sometimes intentionally called with an array in an invalid state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you clarify "sometimes" here, when is that needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently it was just needed in the to_variant
method so i can just change that method to not use to_godot()
, not sure why i didnt see that initially
/// Create a new custom error for a conversion with the value that failed to convert. | ||
pub fn new_with_value<V: fmt::Debug + 'static>(value: V) -> Self { | ||
let mut err = Self::custom(); | ||
err.value = Some(Box::new(value)); | ||
err | ||
} | ||
|
||
/// Create a new custom error with a rust-error as an underlying cause for the conversion error. | ||
pub fn new_with_cause<C: Into<Cause>>(cause: C) -> Self { | ||
let mut err = Self::custom(); | ||
err.cause = Some(cause.into()); | ||
err | ||
} | ||
|
||
/// Create a new custom error with a rust-error as an underlying cause for the conversion error, and the | ||
/// value that failed to convert. | ||
pub fn new_with_cause_value<C: Into<Cause>, V: fmt::Debug + 'static>( | ||
cause: C, | ||
value: V, | ||
) -> Self { | ||
let mut err = Self::custom(); | ||
err.cause = Some(cause.into()); | ||
err.value = Some(Box::new(value)); | ||
err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick, maybe with_*
instead of new_with_*
would be enough?
Interestingly, Rust itself has both 🙈 Vec::with_capacity
and SipHasher::new_with_keys
. Although the former is much more common and not deprecated...
Make `FromGodot` use `Result<Via, ConvertError>` Fix up everything to use the new fallible methods Make itests use `expect` and `expect_err` for fallible conversion testing Add some tests to ensure conversion works as expected
51ab4e0
to
20b0f50
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot!
} | ||
} | ||
|
||
#[derive(Debug, PartialEq, Eq)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still open, but I can address it sometime else, not worth blocking this 😉
quote! { | ||
let #ident = variant.pop_front()? | ||
.try_to::<#field_type>() | ||
.ok()?; | ||
let #ident = variant.pop_front() | ||
.ok_or(ConvertError::with_cause_value("missing expected value", variant.clone()))? | ||
.try_to::<#field_type>()?; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reminder for me to go through quote!
formatting at some point...
Adds
ConvertError
which is used when a conversion fails.Makes
FromGodot
useResult<Self, ConvertError>
instead ofOption<Self>
.The main use for this is for debugging and error messages, as a failed conversion will now communicate what failed and why.
This can also be used for more granular error handling, but at the moment this is fairly limited. We can consider adding more ability to inspect the errors in the future.
I also made each method in the
Signature
traits take the method/function name as the second argument (for consistency) so that error messages always can print what method/function had an error.closes #451