Repository: artichoke/artichoke (Rust)

Issue:
Return Result from ArenaIndex::new
with the addition of `Artichoke::with_ffi_boundary`, this API is now fallible.

propagate an error instead of unwrapping.

use a custom error type that implements `RubyException`.

https://github.com/artichoke/artichoke/blob/e4eb05edb4a08c4463f4a206cb8fb3895ceda6b0/artichoke-backend/src/gc/arena.rs#L35-L39


Discussion:
introduced in GH-661
For a look at how to implement a custom `RubyException`, see `InterpreterAllocError` in `artichoke-backend/src/interpreter.rs`.
I would love to try with this issue
So, I modified the PR per request. Now `ArenaIndex::new` returns a `Result<ArenaIndex, IndexError>` instead of the `InterpreterError`.



Patch:
diff --git a/artichoke-backend/src/block.rs b/artichoke-backend/src/block.rs
index 0651aa25b431..6f44047c4450 100644
--- a/artichoke-backend/src/block.rs
+++ b/artichoke-backend/src/block.rs
@@ -162,7 +162,7 @@ impl Block {
     }
 
     pub fn yield_arg(&self, interp: &mut Artichoke, arg: &Value) -> Result<Value, Exception> {
-        let mut arena = interp.create_arena_savepoint();
+        let mut arena = interp.create_arena_savepoint()?;
 
         let result = unsafe {
             arena
diff --git a/artichoke-backend/src/exception_handler.rs b/artichoke-backend/src/exception_handler.rs
index 56e5303f65ef..bdf1d912881e 100644
--- a/artichoke-backend/src/exception_handler.rs
+++ b/artichoke-backend/src/exception_handler.rs
@@ -10,7 +10,8 @@ use crate::Artichoke;
 ///
 /// This function makes funcalls on the interpreter which are fallible.
 pub fn last_error(interp: &mut Artichoke, exception: Value) -> Result<Exception, Exception> {
-    let mut arena = interp.create_arena_savepoint();
+    let mut arena = interp.create_arena_savepoint()?;
+
     // Clear the current exception from the mruby interpreter so subsequent
     // calls to the mruby VM are not tainted by an error they did not
     // generate.
diff --git a/artichoke-backend/src/gc.rs b/artichoke-backend/src/gc.rs
index d63595683b67..2871628fd3cc 100644
--- a/artichoke-backend/src/gc.rs
+++ b/artichoke-backend/src/gc.rs
@@ -1,3 +1,4 @@
+use crate::gc::arena::IndexError;
 use crate::sys;
 use crate::value::Value;
 use crate::Artichoke;
@@ -19,7 +20,7 @@ pub trait MrbGarbageCollection {
     ///
     /// The returned [`ArenaIndex`] implements [`Drop`], so it is sufficient to
     /// let it go out of scope to ensure objects are eventually collected.
-    fn create_arena_savepoint(&mut self) -> ArenaIndex<'_>;
+    fn create_arena_savepoint(&mut self) -> Result<ArenaIndex<'_>, IndexError>;
 
     /// Retrieve the number of live objects on the interpreter heap.
     ///
@@ -57,7 +58,7 @@ pub trait MrbGarbageCollection {
 }
 
 impl MrbGarbageCollection for Artichoke {
-    fn create_arena_savepoint(&mut self) -> ArenaIndex<'_> {
+    fn create_arena_savepoint(&mut self) -> Result<ArenaIndex<'_>, IndexError> {
         ArenaIndex::new(self)
     }
 
@@ -131,7 +132,7 @@ mod tests {
     fn arena_restore_on_explicit_restore() {
         let mut interp = crate::interpreter().unwrap();
         let baseline_object_count = interp.live_object_count();
-        let mut arena = interp.create_arena_savepoint();
+        let mut arena = interp.create_arena_savepoint().unwrap();
         for _ in 0..2000 {
             let value = arena.eval(b"'a'").unwrap();
             let _ = value.to_s(&mut arena);
@@ -152,7 +153,7 @@ mod tests {
         let mut interp = crate::interpreter().unwrap();
         let baseline_object_count = interp.live_object_count();
         {
-            let mut arena = interp.create_arena_savepoint();
+            let mut arena = interp.create_arena_savepoint().unwrap();
             for _ in 0..2000 {
                 let value = arena.eval(b"'a'").unwrap();
                 let _ = value.to_s(&mut arena);
@@ -172,7 +173,7 @@ mod tests {
     fn enable_disable_gc() {
         let mut interp = crate::interpreter().unwrap();
         interp.disable_gc();
-        let mut arena = interp.create_arena_savepoint();
+        let mut arena = interp.create_arena_savepoint().unwrap();
         let _ = arena
             .interp()
             .eval(
@@ -212,7 +213,7 @@ mod tests {
     #[test]
     fn gc_after_empty_eval() {
         let mut interp = crate::interpreter().unwrap();
-        let mut arena = interp.create_arena_savepoint();
+        let mut arena = interp.create_arena_savepoint().unwrap();
         let baseline_object_count = arena.live_object_count();
         drop(&mut arena.eval(b"").unwrap());
         arena.restore();
@@ -224,9 +225,9 @@ mod tests {
     fn gc_functional_test() {
         let mut interp = crate::interpreter().unwrap();
         let baseline_object_count = interp.live_object_count();
-        let mut initial_arena = interp.create_arena_savepoint();
+        let mut initial_arena = interp.create_arena_savepoint().unwrap();
         for _ in 0..2000 {
-            let mut arena = initial_arena.create_arena_savepoint();
+            let mut arena = initial_arena.create_arena_savepoint().unwrap();
             let result = arena.eval(b"'gc test'");
             let value = result.unwrap();
             assert!(!value.is_dead(&mut arena));
diff --git a/artichoke-backend/src/gc/arena.rs b/artichoke-backend/src/gc/arena.rs
index ad838ddf6dc8..c22ea9426311 100644
--- a/artichoke-backend/src/gc/arena.rs
+++ b/artichoke-backend/src/gc/arena.rs
@@ -1,10 +1,82 @@
 //! Garbage collection arenas for native code.
 
+use std::borrow::Cow;
+use std::error;
+use std::fmt;
 use std::ops::{Deref, DerefMut};
 
+use crate::class_registry::ClassRegistry;
+use crate::core::ConvertMut;
+use crate::exception::{Exception, RubyException};
+use crate::extn::core::exception::Fatal;
 use crate::sys;
 use crate::Artichoke;
 
+/// Failed to extract Artichoke interpreter at an FFI boundary.
+#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct IndexError;
+
+impl IndexError {
+    /// Constructs a new, default `IndexError`.
+    #[must_use]
+    pub fn new() -> Self {
+        Self::default()
+    }
+}
+
+impl fmt::Display for IndexError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "Failed to create internal garbage collection savepoint")
+    }
+}
+
+impl error::Error for IndexError {}
+
+impl RubyException for IndexError {
+    fn message(&self) -> Cow<'_, [u8]> {
+        Cow::Borrowed(b"Failed to create internal garbage collection savepoint")
+    }
+
+    fn name(&self) -> Cow<'_, str> {
+        "fatal".into()
+    }
+
+    fn vm_backtrace(&self, interp: &mut Artichoke) -> Option<Vec<Vec<u8>>> {
+        let _ = interp;
+        None
+    }
+
+    fn as_mrb_value(&self, interp: &mut Artichoke) -> Option<sys::mrb_value> {
+        let message = interp.convert_mut(self.message());
+        let value = interp.new_instance::<Fatal>(&[message]).ok().flatten()?;
+        Some(value.inner())
+    }
+}
+
+impl From<IndexError> for Exception {
+    fn from(exception: IndexError) -> Self {
+        Self::from(Box::<dyn RubyException>::from(exception))
+    }
+}
+
+impl From<Box<IndexError>> for Exception {
+    fn from(exception: Box<IndexError>) -> Self {
+        Self::from(Box::<dyn RubyException>::from(exception))
+    }
+}
+
+impl From<IndexError> for Box<dyn RubyException> {
+    fn from(exception: IndexError) -> Box<dyn RubyException> {
+        Box::new(exception)
+    }
+}
+
+impl From<Box<IndexError>> for Box<dyn RubyException> {
+    fn from(exception: Box<IndexError>) -> Box<dyn RubyException> {
+        exception
+    }
+}
+
 /// Interpreter guard that acts as a GC arena savepoint.
 ///
 /// Arena savepoints ensure mruby objects are reaped even when allocated with
@@ -31,13 +103,13 @@ pub struct ArenaIndex<'a> {
 
 impl<'a> ArenaIndex<'a> {
     /// Create a new arena savepoint.
-    pub fn new(interp: &'a mut Artichoke) -> Self {
-        let index = unsafe {
+    pub fn new(interp: &'a mut Artichoke) -> Result<Self, IndexError> {
+        unsafe {
             interp
                 .with_ffi_boundary(|mrb| sys::mrb_sys_gc_arena_save(mrb))
-                .unwrap_or_default()
-        };
-        Self { index, interp }
+                .map(move |index| Self { index, interp })
+                .map_err(|_| IndexError)
+        }
     }
 
     /// Restore the arena stack pointer to its prior index.
diff --git a/artichoke-backend/src/interpreter.rs b/artichoke-backend/src/interpreter.rs
index 31f615543ffd..2d36ac839a12 100644
--- a/artichoke-backend/src/interpreter.rs
+++ b/artichoke-backend/src/interpreter.rs
@@ -59,7 +59,8 @@ where
     debug!("Succeeded initializing Artichoke Core and Standard Library");
 
     // Load mrbgems
-    let mut arena = interp.create_arena_savepoint();
+    let mut arena = interp.create_arena_savepoint()?;
+
     unsafe {
         arena
             .interp()
@@ -75,7 +76,7 @@ where
     // mruby lazily initializes some core objects like top_self and generates a
     // lot of garbage on startup. Eagerly initialize the interpreter to provide
     // predictable initialization behavior.
-    interp.create_arena_savepoint().interp().eval(&[])?;
+    interp.create_arena_savepoint()?.interp().eval(&[])?;
 
     if let GcState::Enabled = prior_gc_state {
         interp.enable_gc();
diff --git a/artichoke-backend/src/value.rs b/artichoke-backend/src/value.rs
index 7d9e4e711b98..83a038eb1e0a 100644
--- a/artichoke-backend/src/value.rs
+++ b/artichoke-backend/src/value.rs
@@ -136,7 +136,7 @@ impl Value {
         interp: &mut Artichoke,
         len: Int,
     ) -> Result<Option<protect::Range>, Exception> {
-        let mut arena = interp.create_arena_savepoint();
+        let mut arena = interp.create_arena_savepoint()?;
         let result = unsafe {
             arena
                 .interp()
@@ -261,7 +261,7 @@ impl ValueCore for Value {
         args: &[Self::Arg],
         block: Option<Self::Block>,
     ) -> Result<Self::Value, Self::Error> {
-        let mut arena = interp.create_arena_savepoint();
+        let mut arena = interp.create_arena_savepoint()?;
         if let Ok(arg_count_error) = ArgCountError::try_from(args) {
             warn!("{}", arg_count_error);
             return Err(arg_count_error.into());
@@ -615,7 +615,7 @@ mod tests {
     #[test]
     fn is_dead() {
         let mut interp = crate::interpreter().unwrap();
-        let mut arena = interp.create_arena_savepoint();
+        let mut arena = interp.create_arena_savepoint().unwrap();
         let live = arena.eval(b"'dead'").unwrap();
         assert!(!live.is_dead(&mut arena));
         let dead = live;
@@ -632,7 +632,7 @@ mod tests {
     #[test]
     fn immediate_is_dead() {
         let mut interp = crate::interpreter().unwrap();
-        let mut arena = interp.create_arena_savepoint();
+        let mut arena = interp.create_arena_savepoint().unwrap();
         let live = arena.eval(b"27").unwrap();
         assert!(!live.is_dead(&mut arena));
         let immediate = live;


Test patch:
diff --git a/artichoke-backend/tests/leak_unbounded_arena_growth.rs b/artichoke-backend/tests/leak_unbounded_arena_growth.rs
index 8cfbbd1e0d4a..b7f39e69d634 100644
--- a/artichoke-backend/tests/leak_unbounded_arena_growth.rs
+++ b/artichoke-backend/tests/leak_unbounded_arena_growth.rs
@@ -46,7 +46,7 @@ end
         .with_tolerance(LEAK_TOLERANCE)
         .check_leaks(|interp| {
             let code = b"bad_code";
-            let mut arena = interp.create_arena_savepoint();
+            let mut arena = interp.create_arena_savepoint().unwrap();
             let result = arena.eval(code).unwrap_err();
             let backtrace = result.vm_backtrace(&mut arena);
             assert_eq!(expected, backtrace);
@@ -64,7 +64,7 @@ end
         .with_tolerance(LEAK_TOLERANCE)
         .check_leaks_with_finalizer(
             |interp| {
-                let mut arena = interp.create_arena_savepoint();
+                let mut arena = interp.create_arena_savepoint().unwrap();
                 let result = arena.eval(b"'a' * 1024 * 1024").unwrap();
                 let display = result.to_s(&mut arena);
                 assert_eq!(display, expected.as_bytes());
@@ -84,7 +84,7 @@ end
         .with_tolerance(LEAK_TOLERANCE)
         .check_leaks_with_finalizer(
             |interp| {
-                let mut arena = interp.create_arena_savepoint();
+                let mut arena = interp.create_arena_savepoint().unwrap();
                 let result = arena.eval(b"'a' * 1024 * 1024").unwrap();
                 let debug = result.inspect(&mut arena);
                 assert_eq!(debug, expected);