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

Decimal (NOT YET FOR MERGE) #225

Closed
wants to merge 18 commits into from
Closed

Conversation

bheylin
Copy link
Contributor

@bheylin bheylin commented Aug 25, 2020

This is a draft implementation of integrating a Decimal type that depends on ['no_float'] being set.

The ['no_float'] dependency is required because the Decimal type hijacks the float parser as the Rhai language has no support for typed literals. Without a typed literal, the parser has no way to decide whether to store a decimal literal as a float or a Decimal.

I use the rust_decimal library as the Decimal implementation as it is pure Rust and still being maintained.

ObsceneGiraffe added 8 commits August 24, 2020 10:02
…l create was the choosen implementation, but this is liable to change.

As it stands, the decimal type works, but only when 'no_float' is set, as the Decimal type needs to hijack the float parser since there isn't typed literals in the Rhai language.
Replaced the arithmetic operators defined in arithmetics.rs and logic.rs with efficient hard coded versions in fn_call.rs
Added tests for Decimal array usage.
Added support for use of decimals in arrays in array_basic.rs
Added 'no_float' as a dependency of 'decimal' as the decimal feature
hijacks the float parser, since the Rhai language has no way to
distinguish between float literals and decimal literals.
@bheylin bheylin changed the title Decimal (NOT YET READY FOR MERGE) Decimal (NOT YET FOR MERGE) Aug 25, 2020
Copy link
Collaborator

@schungx schungx left a comment

Choose a reason for hiding this comment

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

This is great! Some comments in this review.

JSON numbers are all floating-point while Rhai supports integers (`INT`) and floating-point (`FLOAT`) if
the [`no_float`] feature is not used. Most common generators of JSON data distinguish between
JSON numbers are all floating-point while Rhai supports integers (`INT`) floating-point (`FLOAT`) if
the [`no_float`] feature is not used and decimal is the [`decimal`] feature is used.
Copy link
Collaborator

Choose a reason for hiding this comment

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

and decimal if the [decimal] feature is used

@@ -10,6 +10,9 @@ The default system integer type (also aliased to `INT`) is `i64`. It can be turn
Floating-point numbers are also supported if not disabled with [`no_float`]. The default system floating-point type is `i64`
(also aliased to `FLOAT`).

Decimal numbers are also supported if explicitly enabled with ['decimal'].
Be aware that Decimal support replaces float support, the two features cannot be enabled at the same time.
Copy link
Collaborator

Choose a reason for hiding this comment

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

add: "... meaning that [decimal] implies [no_float]." to be explicitly clear?

@@ -32,6 +33,8 @@ If only 32-bit integers are needed, enabling the [`only_i32`] feature will remov
This is useful on some 32-bit targets where using 64-bit integers incur a performance penalty.

If no floating-point is needed or supported, use the [`no_float`] feature to remove it.
If decimal support is necessary, use the [`decimal`] feature, but be aware that decimal and float support cannot cco-exist in the same build.
Copy link
Collaborator

Choose a reason for hiding this comment

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

cco-exist

@@ -31,7 +31,7 @@ Opt-Out of Features
------------------

Opt out of as many features as possible, if they are not needed, to reduce code size because, remember, by default
all code is compiled into the final binary since what a script requires cannot be predicted.
all code except [`decimal`] is compiled into the final binary since what a script requires cannot be predicted.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Well, actually there are a few opt-in features, such as serde. So this statement is not technically correct.

@@ -17,6 +17,7 @@ more control over what a script can (or cannot) do.
| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and [`AST`], are all `Send + Sync`. |
| `no_optimize` | Disable [script optimization]. |
| `no_float` | Disable floating-point numbers and math. |
| `decimal` | Enable decimal number support (through the 'rust_decimal' create). This will disable floating point support.
Copy link
Collaborator

Choose a reason for hiding this comment

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

create -> crate

src/any.rs Outdated

#[cfg(feature = "decimal")]
{
boxed = match unsafe_cast_box::<_, rust_decimal::Decimal>(boxed) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I suppose Decimal is Copy so it can use unsafe_try_cast to save on one boxing operation?

Copy link
Collaborator

@schungx schungx Aug 26, 2020

Choose a reason for hiding this comment

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

Maybe just copy the FLOAT block:

        #[cfg(feature = "decimal")]
        if type_id == TypeId::of::<Decimal>() {
            return <dyn Any>::downcast_ref::<Decimal>(&value)
                .unwrap()
                .clone()
                .into();
        }

Copy link
Collaborator

@schungx schungx Aug 26, 2020

Choose a reason for hiding this comment

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

Also, if you have serde enabled, it may attempt to serialize f32 or f64 into a Dynamic via Dynamic::from. Therefore, you'd want to also handle the case where 1) no_float, 2) decimal, 3) serde (which I suppose must have floats...), 4) the type is f32 or f64. In such a case, you should convert the floating-point number into Decimal.:

        #[cfg(feature = "decimal")]
        #[cfg(feature = "serde")]
        if type_id == TypeId::of::<f64>() {
            return dec!(<dyn Any>::downcast_ref::<f64>(&value).unwrap().clone()).into();
        } else if type_id == TypeId::of::<f32>() {
            return dec!(<dyn Any>::downcast_ref::<f32>(&value).unwrap().clone()).into();
        }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using unsafe_try_cast is possible, but this breaks the transfer of ownership pattern at play, where the Result::Err<Box<...> returned from unsafe_cast_box is used to move the boxed value back from the unsafe_try_cast.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem being that unsafe_try_cast returns an Option meaning it consumes the boxed value. I'm unsure how to resolve that. Maybe by borrowing the contents of the Box to unsafe_try_cast?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, my mistake. You can't use unsafe_try_cast here. Just downcast_ref and then clone as my example.

Copy link
Collaborator

Choose a reason for hiding this comment

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

They key points are: 1) Decimal is Copy so cloning is cheaper than boxing (supposedly), 2) You should handle the rate conversion cases to/from f32/f64.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

downcast_ref version is pushed.

I have to look at the serde case closer later on. I need to understand the flow there. rust_decimal has serde support.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes. I think it probably defaults to serialize to/from JSON as f64 or f32. Thus you need to consider some of the plumbing in the serde feature that serializes to/from Dynamic.

However, it is not a big deal, as I can clean them up later on.

src/any.rs Outdated
@@ -692,6 +716,14 @@ impl Dynamic {
};
}

#[cfg(feature = "decimal")]
if type_id == TypeId::of::<Decimal>() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, consider unsafe_try_cast if Decimal is Copy

Copy link
Collaborator

Choose a reason for hiding this comment

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

unsafe_try_cast here should be fine!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

unsafe_try_cast version is pushed.

@@ -1203,6 +1206,27 @@ pub fn run_builtin_binary_op(
}
}

#[cfg(feature = "decimal")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't know if a Decimal can overflow or underflow. You may want to actually copy the INT code block instead of the FLOAT code block if you need checked arithmetic... At least the division should trap divide-by-zero...

Copy link
Contributor Author

@bheylin bheylin Aug 26, 2020

Choose a reason for hiding this comment

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

I'll have to take a look at how this all fits together a bit closer. Decimal can overflow, but it has checked versions for add, sub, etc that return an Option.

Rhai also has Checked* traits that return an Option, so all good there. I'll see later on today if I can use those traits for the Decimal type.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, look into the add, sub etc. functions. Those are checked versions. You can copy that template for your own add_decimal etc. Essentially if it overflows, convert it into an error.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Well, actually I see that Decimal seems to implement num_traits::CheckedAdd etc. If so, maybe you can simply use the generic versions of add, sub etc. and no need to write your own versions.

@bheylin
Copy link
Contributor Author

bheylin commented Aug 26, 2020 via email

@bheylin
Copy link
Contributor Author

bheylin commented Aug 26, 2020

@schungx Ok I'm gonna be busy with adulting for the rest of the day. I'll come back to the open issues tomorrow morning. have a good one 😉

@bheylin
Copy link
Contributor Author

bheylin commented Sep 1, 2020

Hey hey, after an unexpected break I'm back in business ;)

@schungx
Copy link
Collaborator

schungx commented Sep 1, 2020

Great to have you back! But a bit of a "doh!"...

I just merged in a major revision that implements a plugins system with procedural macros.

The files under packages have been changed significantly. You may have to do some revision to merge into that.

Or you can leave it to me... I can apply your changes to the new code base when you're ready.

Sorry!

@bheylin
Copy link
Contributor Author

bheylin commented Sep 1, 2020

Nah don't worry about it, I'll merge in the changes ;)

@schungx
Copy link
Collaborator

schungx commented Sep 4, 2020

@ObsceneGiraffe let me know when this can be merged.

@bheylin
Copy link
Contributor Author

bheylin commented Sep 4, 2020

@schungx well all the tests are passing. I merged the changes in upstream yesterday. But the Serde work remains to be done. I'm starting that now.

@bheylin
Copy link
Contributor Author

bheylin commented Sep 4, 2020

I took a look at splitting the gen_arithmetic_functions marco up into gen_bitwise_functions so that I could use that for defining the Decimal functions too. I had some trouble making the if y == 0 test generic in the fn divide().

Decimal implemented trait Zero but all the integer types do not for some reason?

But anyways, I'm going stash that work and get on with the serde / Decimal integration.

@schungx
Copy link
Collaborator

schungx commented Sep 4, 2020

But anyways, I'm going stash that work and get on with the serde / Decimal integration.

No prob.

I took a look at splitting the gen_arithmetic_functions marco up into gen_bitwise_functions so that I could use that for defining the Decimal functions too.

You can, but gen_arithmetic_functions currently is registered in three groups. If you split it, then it doubles to six groups.

May not be worth it, as having a dedicated module like what you have is perhaps simpler.

I had some trouble making the if y == 0 test generic in the fn divide().

This is why Zero is gone: rust-lang/rfcs#369

I'd hesitated to pull in num_traits just to have it though...

@bheylin
Copy link
Contributor Author

bheylin commented Sep 4, 2020

You can, but gen_arithmetic_functions currently is registered in three groups. If you split it, then it doubles to six groups.

May not be worth it, as having a dedicated module like what you have is perhaps simpler.

Yea the increase in groups is not worth the trivial duplication in the decimal_functions module.

This is why Zero is gone: rust-lang/rfcs#369

And good to know

@bheylin
Copy link
Contributor Author

bheylin commented Sep 4, 2020

On the serde support. I don't think the Dynamic type should be modified to support converting floats to Decimal, as that's an inherently dangerous operation in terms of loss of precision.

rust_decimal itself has implementations of serde::Serialize and serde::Deserialize traits. The rust_decimal implementations convert directly to and from string by default unless serde_float is enabled.

From what I see, I would have to integrate the Decimal impls for serde::Serialize and serde::Deserialize into the Rhai DynamicDeserializer and DynamicSerializer in de.rs and ser.rs respectively.

@schungx
Copy link
Collaborator

schungx commented Sep 4, 2020

However, I think the serde serialization trait requires you to implement a serializer for f32 and f64... I'm not particularly fluent with serde, so maybe you can just unimplemented!() them.

@bheylin
Copy link
Contributor Author

bheylin commented Sep 4, 2020

Yep serde is new for me too. But that's where I'm heading with the serde support.

@schungx
Copy link
Collaborator

schungx commented Sep 4, 2020

A reminder: Rhai serde support is essentially a new serialization format. Dynamic is a format (like JSON), which things can serialize into.

That's why it is ser::to_dynamic and de::from_dynamic and not the other way round.

@bheylin
Copy link
Contributor Author

bheylin commented Sep 14, 2020

@schungx I'm having some trouble figuring out how to integrate the serde definitions in rust_decimal into the Deserializer impl in de.rs:160.

rust_decimal has various serde::Deserialize impls in serde_types.rs and it looks like I need to integrate that Deserialize impl into the match statement in de.rs:160. It's unclear to me what I need to do here. Should I somehow create a new instance of rust_decimal::serde_types::DecimalVisitor? Or should I directly call Deserialize::deserialize directly on the Decimal type.

My serde knowledge is quite limited, I could so with help from someone with more knowledge of serde. Can you put me in contact with someone to discuss this integration?

@schungx
Copy link
Collaborator

schungx commented Sep 14, 2020

I'm not an expert in serde also... Actually I did the ground work and somebody else refined it...

Deserialization means going from Dynamic to Decimal. Maybe it doesn't need any special treatment... because serde only understands f32 and f64... So it can be Dynamic -> f64 -> Decimal, but then you may have rounding errors...

Let me think about this some more...

@bheylin
Copy link
Contributor Author

bheylin commented Sep 14, 2020

Yea that's what I'm trying to avoid. I want to warn if someone is trying to deserialize from floats to Decimal. I explicitly want to only handle string -> Decimal

@schungx
Copy link
Collaborator

schungx commented Sep 14, 2020

If there is built-in support for String -> Decimal, then it is simple...

Just do:

Union::Decimal(_) => self.deserialize_str(visitor),

fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Box<EvalAltResult>> {
    if self.value.type_id() == TypeId::of::<Decimal>() {
        // ... get the Decimal, then convert into a string
    } else {
        self.value.downcast_ref::<ImmutableString>().map_or_else(|| self.type_error(), |x| visit_borrowed_str(x.as_str()))
    }
}

That would convert a Dynamic value with Decimal type into a string for serde. Then, serde will convert it into your Decimal from that string, if that's what you plan to convert to...

Or you may want to use u64 or u128 as the conversion payload to avoid allocations, or a byte array...

@schungx
Copy link
Collaborator

schungx commented Feb 27, 2021

Decimal is now built into Rhai. Retiring this PR.

@schungx schungx closed this Feb 27, 2021
@bheylin
Copy link
Contributor Author

bheylin commented Mar 25, 2021

Thanks @schungx, apologies, I disappeared into the wilderness for a while.

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