artichoke_backend/
types.rs

1pub use crate::core::{Ruby, Rust};
2use crate::sys;
3
4/// Parse a [`Ruby`] type classifier from a [`sys::mrb_value`].
5///
6/// This function collapses some mruby types into a [`Ruby::Unreachable`] type
7/// that force an interaction with the VM to return an error.
8#[must_use]
9pub fn ruby_from_mrb_value(value: sys::mrb_value) -> Ruby {
10    use sys::mrb_vtype::{
11        MRB_TT_ARRAY, MRB_TT_BIGINT, MRB_TT_BREAK, MRB_TT_CDATA, MRB_TT_CLASS, MRB_TT_COMPLEX, MRB_TT_CPTR,
12        MRB_TT_ENV, MRB_TT_EXCEPTION, MRB_TT_FALSE, MRB_TT_FIBER, MRB_TT_FLOAT, MRB_TT_FREE, MRB_TT_HASH,
13        MRB_TT_ICLASS, MRB_TT_INTEGER, MRB_TT_ISTRUCT, MRB_TT_MAXDEFINE, MRB_TT_MODULE, MRB_TT_OBJECT, MRB_TT_PROC,
14        MRB_TT_RANGE, MRB_TT_RATIONAL, MRB_TT_SCLASS, MRB_TT_STRING, MRB_TT_STRUCT, MRB_TT_SYMBOL, MRB_TT_TRUE,
15        MRB_TT_UNDEF,
16    };
17
18    #[expect(clippy::match_same_arms, reason = "match arms are ordered by `sys::mrb_vtype` enum")]
19    match value.tt {
20        // `nil` is implemented with the `MRB_TT_FALSE` type tag in mruby
21        // (since both values are falsy). The difference is that Booleans are
22        // non-zero `Fixnum`s.
23        MRB_TT_FALSE if sys::mrb_sys_value_is_nil(value) => Ruby::Nil,
24        MRB_TT_FALSE => Ruby::Bool,
25        // `MRB_TT_FREE` is a marker type tag that indicates to the mruby
26        // VM that an object is unreachable and should be deallocated by the
27        // garbage collector.
28        MRB_TT_FREE => Ruby::Unreachable,
29        MRB_TT_TRUE => Ruby::Bool,
30        MRB_TT_INTEGER => Ruby::Fixnum,
31        MRB_TT_SYMBOL => Ruby::Symbol,
32        // internal use: `#undef`; should not happen
33        MRB_TT_UNDEF => Ruby::Unreachable,
34        MRB_TT_FLOAT => Ruby::Float,
35        // `MRB_TT_CPTR` wraps a borrowed `void *` pointer.
36        MRB_TT_CPTR => Ruby::CPointer,
37        MRB_TT_OBJECT => Ruby::Object,
38        MRB_TT_CLASS => Ruby::Class,
39        MRB_TT_MODULE => Ruby::Module,
40        // `MRB_TT_ICLASS` is an internal use type tag meant for holding
41        // mixed in modules.
42        MRB_TT_ICLASS => Ruby::Unreachable,
43        // `MRB_TT_SCLASS` represents a singleton class, or a class that is
44        // defined anonymously, e.g. `c1` or `c2` below:
45        //
46        // ```ruby
47        // c1 = Class.new {
48        //   def foo; :foo; end
49        // }
50        // c2 = (class <<cls; self; end)
51        // ```
52        //
53        // mruby also uses the term singleton method to refer to methods
54        // defined on an object's eigenclass, e.g. `bar` below:
55        //
56        // ```ruby
57        // class Foo; end
58        // obj = Foo.new
59        // def obj.bar; 'bar'; end
60        // ```
61        MRB_TT_SCLASS => Ruby::SingletonClass,
62        MRB_TT_PROC => Ruby::Proc,
63        // `MRB_TT_ARRAY` refers to the mruby `mrb_array` implementation.
64        // Artichoke implements its own `Array` as a `Ruby::Data`, so this
65        // variant is unreachable.
66        MRB_TT_ARRAY => Ruby::Array,
67        MRB_TT_HASH => Ruby::Hash,
68        MRB_TT_STRING => Ruby::String,
69        MRB_TT_RANGE => Ruby::Range,
70        MRB_TT_EXCEPTION => Ruby::Exception,
71        // NOTE(lopopolo): This might be an internal closure symbol table,
72        // rather than the `ENV` core object.
73        MRB_TT_ENV => Ruby::Unreachable,
74        // `MRB_TT_CDATA` is a type tag for wrapped C pointers. It is used
75        // to indicate that an `mrb_value` has an owned pointer to an
76        // external data structure stored in its `value.p` field.
77        MRB_TT_CDATA => Ruby::Data,
78        // NOTE(lopopolo): `Fiber`s are unimplemented in Artichoke.
79        MRB_TT_FIBER => Ruby::Fiber,
80        MRB_TT_STRUCT => Ruby::Unreachable,
81        // `MRB_TT_ISTRUCT` is an "inline structure", or a `mrb_value` that
82        // stores data in a `char*` buffer inside an `mrb_value`. These
83        // `mrb_value`s cannot have a finalizer and cannot have instance
84        // variables.
85        //
86        // See `vendor/mruby-*/include/mruby/istruct.h`.
87        MRB_TT_ISTRUCT => Ruby::InlineStruct,
88        // `MRB_TT_BREAK` is used internally to the mruby VM. BREAK is used as
89        // the return value of `mrb_yield` when the block has a non-local
90        // return.
91        //
92        // FIXME(lopopolo): The below "unreachable" designation is incorrect.
93        // BREAK should be handled by `sys::protect::block_yield`.
94        MRB_TT_BREAK => Ruby::Unreachable,
95        MRB_TT_COMPLEX => Ruby::Unreachable,
96        MRB_TT_RATIONAL => Ruby::Unreachable,
97        MRB_TT_BIGINT => Ruby::Unreachable,
98        // `MRB_TT_MAXDEFINE` is a marker enum value used by the mruby VM to
99        // dynamically check if a type tag is valid using the less than
100        // operator. It does not correspond to an instantiated type.
101        MRB_TT_MAXDEFINE => Ruby::Unreachable,
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use std::collections::HashMap;
108
109    use crate::test::prelude::*;
110    use crate::types;
111
112    #[test]
113    fn parse_nil_ruby_type() {
114        let nil = Value::nil();
115        assert_eq!(Ruby::Nil, types::ruby_from_mrb_value(nil.inner()));
116    }
117
118    #[test]
119    fn parse_bool_ruby_type() {
120        let interp = interpreter();
121        let yes = interp.convert(true);
122        assert_eq!(Ruby::Bool, types::ruby_from_mrb_value(yes.inner()));
123        let no = interp.convert(false);
124        assert_eq!(Ruby::Bool, types::ruby_from_mrb_value(no.inner()));
125    }
126
127    #[test]
128    fn parse_fixnum_ruby_type() {
129        let interp = interpreter();
130        let zero = interp.convert(0_i64);
131        assert_eq!(Ruby::Fixnum, types::ruby_from_mrb_value(zero.inner()));
132        let thousand = interp.convert(1000_i64);
133        assert_eq!(Ruby::Fixnum, types::ruby_from_mrb_value(thousand.inner()));
134    }
135
136    #[test]
137    fn parse_symbol_ruby_type() {
138        let mut interp = interpreter();
139        let empty = interp.eval(b":''").unwrap();
140        assert_eq!(Ruby::Symbol, types::ruby_from_mrb_value(empty.inner()));
141        let utf8 = interp.eval(b":Artichoke").unwrap();
142        assert_eq!(Ruby::Symbol, types::ruby_from_mrb_value(utf8.inner()));
143        let binary = interp.eval(br#":"\xFE""#).unwrap();
144        assert_eq!(Ruby::Symbol, types::ruby_from_mrb_value(binary.inner()));
145    }
146
147    #[test]
148    fn parse_float_ruby_type() {
149        let mut interp = interpreter();
150        let zero = interp.convert_mut(0.0_f64);
151        assert_eq!(Ruby::Float, types::ruby_from_mrb_value(zero.inner()));
152        let float = interp.convert_mut(1.5_f64);
153        assert_eq!(Ruby::Float, types::ruby_from_mrb_value(float.inner()));
154    }
155
156    #[test]
157    fn parse_object_ruby_type() {
158        let mut interp = interpreter();
159        let object = interp.eval(b"Object.new").unwrap();
160        assert_eq!(Ruby::Object, types::ruby_from_mrb_value(object.inner()));
161        #[cfg(feature = "core-env")]
162        {
163            let env = interp.eval(b"ENV").unwrap();
164            assert_eq!(Ruby::Object, types::ruby_from_mrb_value(env.inner()));
165        }
166    }
167
168    #[test]
169    fn parse_class_ruby_type() {
170        let mut interp = interpreter();
171        let builtin = interp.eval(b"Object").unwrap();
172        assert_eq!(Ruby::Class, types::ruby_from_mrb_value(builtin.inner()));
173        let data = interp.eval(b"Array").unwrap();
174        assert_eq!(Ruby::Class, types::ruby_from_mrb_value(data.inner()));
175        #[cfg(feature = "core-math")]
176        {
177            let source = interp.eval(b"Math::DomainError").unwrap();
178            assert_eq!(Ruby::Class, types::ruby_from_mrb_value(source.inner()));
179        }
180    }
181
182    #[test]
183    fn parse_module_ruby_type() {
184        let mut interp = interpreter();
185        let builtin = interp.eval(b"Comparable").unwrap();
186        assert_eq!(Ruby::Module, types::ruby_from_mrb_value(builtin.inner()));
187        #[cfg(feature = "core-math")]
188        {
189            let data = interp.eval(b"Math").unwrap();
190            assert_eq!(Ruby::Module, types::ruby_from_mrb_value(data.inner()));
191        }
192        let artichoke = interp.eval(b"Artichoke").unwrap();
193        assert_eq!(Ruby::Module, types::ruby_from_mrb_value(artichoke.inner()));
194    }
195
196    #[test]
197    fn parse_proc_ruby_type() {
198        let mut interp = interpreter();
199        let literal = interp.eval(b"proc {}").unwrap();
200        assert_eq!(Ruby::Proc, types::ruby_from_mrb_value(literal.inner()));
201        let proc = interp.eval(b"Proc.new {}").unwrap();
202        assert_eq!(Ruby::Proc, types::ruby_from_mrb_value(proc.inner()));
203        let lambda = interp.eval(b"lambda {}").unwrap();
204        assert_eq!(Ruby::Proc, types::ruby_from_mrb_value(lambda.inner()));
205        let stabby = interp.eval(b"->() {}").unwrap();
206        assert_eq!(Ruby::Proc, types::ruby_from_mrb_value(stabby.inner()));
207    }
208
209    #[test]
210    fn parse_string_ruby_type() {
211        let mut interp = interpreter();
212        let empty = interp.try_convert_mut("").unwrap();
213        assert_eq!(Ruby::String, types::ruby_from_mrb_value(empty.inner()));
214        let utf8 = interp.try_convert_mut("Artichoke").unwrap();
215        assert_eq!(Ruby::String, types::ruby_from_mrb_value(utf8.inner()));
216        let binary = interp.try_convert_mut(vec![0xFF_u8, 0x00, 0xFE]).unwrap();
217        assert_eq!(Ruby::String, types::ruby_from_mrb_value(binary.inner()));
218    }
219
220    #[test]
221    fn parse_array_ruby_type() {
222        let mut interp = interpreter();
223        let empty = interp.eval(b"[]").unwrap();
224        assert_eq!(Ruby::Array, types::ruby_from_mrb_value(empty.inner()));
225        #[cfg(feature = "core-regexp")]
226        {
227            let array = interp.eval(b"[1, /./, Object.new]").unwrap();
228            assert_eq!(Ruby::Array, types::ruby_from_mrb_value(array.inner()));
229        }
230        let ary = vec!["a", "b", "c"];
231        let converted = interp.try_convert_mut(ary).unwrap();
232        assert_eq!(Ruby::Array, types::ruby_from_mrb_value(converted.inner()));
233    }
234
235    #[test]
236    fn parse_hash_ruby_type() {
237        let mut interp = interpreter();
238        let empty = interp.eval(b"{}").unwrap();
239        assert_eq!(Ruby::Hash, types::ruby_from_mrb_value(empty.inner()));
240        #[cfg(feature = "core-regexp")]
241        {
242            let hash = interp.eval(b"{a: 1, b: [/./]}").unwrap();
243            assert_eq!(Ruby::Hash, types::ruby_from_mrb_value(hash.inner()));
244        }
245        let mut map = HashMap::default();
246        map.insert(b"a".to_vec(), vec![0_u8]);
247        map.insert(b"b".to_vec(), b"binary".to_vec());
248        let converted = interp.try_convert_mut(map).unwrap();
249        assert_eq!(Ruby::Hash, types::ruby_from_mrb_value(converted.inner()));
250    }
251
252    #[test]
253    fn parse_range_ruby_type() {
254        let mut interp = interpreter();
255        let dot2 = interp.eval(b"0..0").unwrap();
256        assert_eq!(Ruby::Range, types::ruby_from_mrb_value(dot2.inner()));
257        let dot3 = interp.eval(b"0...0").unwrap();
258        assert_eq!(Ruby::Range, types::ruby_from_mrb_value(dot3.inner()));
259    }
260
261    #[test]
262    fn parse_exception_ruby_type() {
263        let mut interp = interpreter();
264        let root = interp.eval(b"Exception.new").unwrap();
265        assert_eq!(Ruby::Exception, types::ruby_from_mrb_value(root.inner()));
266        let stderror = interp.eval(b"StandardError.new").unwrap();
267        assert_eq!(Ruby::Exception, types::ruby_from_mrb_value(stderror.inner()));
268        let index = interp.eval(b"IndexError.new").unwrap();
269        assert_eq!(Ruby::Exception, types::ruby_from_mrb_value(index.inner()));
270        #[cfg(feature = "core-math")]
271        {
272            let domain = interp.eval(b"Math::DomainError.new").unwrap();
273            assert_eq!(Ruby::Exception, types::ruby_from_mrb_value(domain.inner()));
274        }
275    }
276}