artichoke_backend/extn/core/numeric/
mod.rs

1use crate::extn::core::integer::Integer;
2use crate::extn::prelude::*;
3
4pub(in crate::extn) mod mruby;
5
6#[derive(Debug, Clone, Copy)]
7pub struct Numeric;
8
9#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
10pub enum Outcome {
11    Float(f64),
12    Integer(i64),
13    // TODO: Complex? Rational?
14}
15
16impl ConvertMut<Outcome, Value> for Artichoke {
17    fn convert_mut(&mut self, from: Outcome) -> Value {
18        match from {
19            Outcome::Float(num) => self.convert_mut(num),
20            Outcome::Integer(num) => self.convert(num),
21        }
22    }
23}
24
25const MAX_COERCE_DEPTH: u8 = 15;
26
27#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
28pub enum Coercion {
29    Float(f64, f64),
30    Integer(i64, i64),
31    // TODO: Complex? Rational?
32}
33
34/// If `y` is the same type as `x`, returns an array `[y, x]`. Otherwise,
35/// returns an array with both `y` and `x` represented as `Float` objects.
36///
37/// This coercion mechanism is used by Ruby to handle mixed-type numeric
38/// operations: it is intended to find a compatible common type between the two
39/// operands of the operator.
40///
41/// See [`Numeric#coerce`][numeric].
42///
43/// # Coercion enum
44///
45/// Artichoke represents the `[y, x]` tuple Array as the [`Coercion`] enum, which
46/// orders its values `Coercion::Integer(x, y)`.
47///
48/// [numeric]: https://ruby-doc.org/core-3.1.2/Numeric.html#method-i-coerce
49pub fn coerce(interp: &mut Artichoke, x: Value, y: Value) -> Result<Coercion, Error> {
50    fn do_coerce(interp: &mut Artichoke, x: Value, y: Value, depth: u8) -> Result<Coercion, Error> {
51        if depth > MAX_COERCE_DEPTH {
52            return Err(SystemStackError::with_message("stack level too deep").into());
53        }
54        match (x.ruby_type(), y.ruby_type()) {
55            (Ruby::Float, Ruby::Float) => Ok(Coercion::Float(
56                x.try_convert_into(interp)?,
57                y.try_convert_into(interp)?,
58            )),
59            (Ruby::Float, Ruby::Fixnum) => {
60                let y = y.try_convert_into::<Integer>(interp)?;
61                Ok(Coercion::Float(x.try_convert_into(interp)?, y.as_f64()))
62            }
63            (Ruby::Fixnum, Ruby::Float) => {
64                let x = x.try_convert_into::<Integer>(interp)?;
65                Ok(Coercion::Float(x.as_f64(), y.try_convert_into(interp)?))
66            }
67            (Ruby::Fixnum, Ruby::Fixnum) => Ok(Coercion::Integer(
68                x.try_convert_into(interp)?,
69                y.try_convert_into(interp)?,
70            )),
71            _ => {
72                let class_of_numeric = interp
73                    .class_of::<Numeric>()?
74                    .ok_or_else(|| NotDefinedError::class("Numeric"))?;
75                let is_a_numeric = y.funcall(interp, "is_a?", &[class_of_numeric], None)?;
76                let is_a_numeric = interp.try_convert(is_a_numeric);
77                if let Ok(true) = is_a_numeric {
78                    if y.respond_to(interp, "coerce")? {
79                        let coerced = y.funcall(interp, "coerce", &[x], None)?;
80                        let coerced: Vec<Value> = interp
81                            .try_convert_mut(coerced)
82                            .map_err(|_| TypeError::with_message("coerce must return [x, y]"))?;
83                        let mut coerced = coerced.into_iter();
84                        let y = coerced
85                            .next()
86                            .ok_or_else(|| TypeError::with_message("coerce must return [x, y]"))?;
87                        let x = coerced
88                            .next()
89                            .ok_or_else(|| TypeError::with_message("coerce must return [x, y]"))?;
90                        if coerced.next().is_some() {
91                            Err(TypeError::with_message("coerce must return [x, y]").into())
92                        } else {
93                            do_coerce(interp, x, y, depth + 1)
94                        }
95                    } else {
96                        let mut message = String::from("can't convert ");
97                        message.push_str(interp.inspect_type_name_for_value(y));
98                        message.push_str(" into Float");
99                        Err(TypeError::from(message).into())
100                    }
101                } else {
102                    let mut message = String::from(interp.inspect_type_name_for_value(y));
103                    message.push_str(" can't be coerced into Float");
104                    Err(TypeError::from(message).into())
105                }
106            }
107        }
108    }
109    do_coerce(interp, x, y, 0)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::Coercion;
115    use crate::test::prelude::*;
116
117    #[test]
118    fn coerce_int_to_float() {
119        let mut interp = interpreter();
120        let x = interp.convert(1_i64);
121        let y = interp.convert_mut(2.5_f64);
122        assert_eq!(Coercion::Float(1.0, 2.5), super::coerce(&mut interp, x, y).unwrap());
123    }
124
125    #[test]
126    fn coerce_float_to_int() {
127        let mut interp = interpreter();
128        let x = interp.convert_mut(1.2_f64);
129        let y = interp.convert(3_i64);
130        assert_eq!(Coercion::Float(1.2, 3.0), super::coerce(&mut interp, x, y).unwrap());
131    }
132
133    #[test]
134    fn coerce_int_to_int() {
135        let mut interp = interpreter();
136        let x = interp.convert(1_i64);
137        let y = interp.convert(2_i64);
138        assert_eq!(Coercion::Integer(1, 2), super::coerce(&mut interp, x, y).unwrap());
139    }
140
141    #[test]
142    fn coerce_float_to_float() {
143        let mut interp = interpreter();
144        let x = interp.convert_mut(1.2_f64);
145        let y = interp.convert_mut(2.5_f64);
146        assert_eq!(Coercion::Float(1.2, 2.5), super::coerce(&mut interp, x, y).unwrap());
147    }
148}