use log::{error, trace, warn};
use std::ffi::{c_void, CString};
use std::mem;
use std::rc::Rc;
use crate::exception::{LastError, MrbExceptionHandler};
use crate::sys::{self, DescribeState};
use crate::value::Value;
use crate::{Mrb, MrbError};
const TOP_FILENAME: &str = "(eval)";
struct Protect {
ctx: *mut sys::mrbc_context,
code: Vec<u8>,
}
impl Protect {
fn new(interp: &Mrb, code: &[u8]) -> Self {
Self {
ctx: interp.borrow().ctx,
code: code.to_vec(),
}
}
unsafe extern "C" fn run_protected(
mrb: *mut sys::mrb_state,
data: sys::mrb_value,
) -> sys::mrb_value {
let ptr = sys::mrb_sys_cptr_ptr(data);
let args = Rc::from_raw(ptr as *const Self);
let ctx = args.ctx;
let code = args.code.as_ptr();
let len = args.code.len();
drop(args);
sys::mrb_load_nstring_cxt(mrb, code as *const i8, len, ctx)
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvalContext {
pub filename: String,
}
impl EvalContext {
pub fn new<T>(filename: T) -> Self
where
T: AsRef<str>,
{
Self {
filename: filename.as_ref().to_owned(),
}
}
pub fn root() -> Self {
Self::default()
}
}
impl Default for EvalContext {
fn default() -> Self {
Self {
filename: TOP_FILENAME.to_owned(),
}
}
}
#[allow(clippy::module_name_repetitions)]
pub trait MrbEval {
fn eval<T>(&self, code: T) -> Result<Value, MrbError>
where
T: AsRef<[u8]>;
fn unchecked_eval<T>(&self, code: T) -> Value
where
T: AsRef<[u8]>;
fn eval_with_context<T>(&self, code: T, context: EvalContext) -> Result<Value, MrbError>
where
T: AsRef<[u8]>;
fn unchecked_eval_with_context<T>(&self, code: T, context: EvalContext) -> Value
where
T: AsRef<[u8]>;
fn peek_context(&self) -> Option<EvalContext>;
fn push_context(&self, context: EvalContext);
fn pop_context(&self);
}
impl MrbEval for Mrb {
fn eval<T>(&self, code: T) -> Result<Value, MrbError>
where
T: AsRef<[u8]>,
{
let (mrb, ctx) = {
let borrow = self.borrow();
(borrow.mrb, borrow.ctx)
};
let context = {
let api = self.borrow();
if let Some(context) = api.context_stack.last() {
context.clone()
} else {
EvalContext::root()
}
};
if let Ok(cfilename) = CString::new(context.filename.to_owned()) {
unsafe {
sys::mrbc_filename(mrb, ctx, cfilename.as_ptr() as *const i8);
}
} else {
warn!("Could not set {} as mrc context filename", context.filename);
}
drop(context);
let args = Rc::new(Protect::new(self, code.as_ref()));
drop(code);
trace!("Evaling code on {}", mrb.debug());
let value = unsafe {
let data = sys::mrb_sys_cptr_value(mrb, Rc::into_raw(Rc::clone(&args)) as *mut c_void);
let mut state = <mem::MaybeUninit<sys::mrb_bool>>::uninit();
let value =
sys::mrb_protect(mrb, Some(Protect::run_protected), data, state.as_mut_ptr());
drop(args);
if state.assume_init() != 0 {
(*mrb).exc = sys::mrb_sys_obj_ptr(value);
}
value
};
let value = Value::new(self, value);
match self.last_error() {
LastError::Some(exception) => {
warn!("runtime error with exception backtrace: {}", exception);
Err(MrbError::Exec(exception.to_string()))
}
LastError::UnableToExtract(err) => {
error!("failed to extract exception after runtime error: {}", err);
Err(err)
}
LastError::None if value.is_unreachable() => {
Err(MrbError::UnreachableValue(value.inner().tt))
}
LastError::None => Ok(value),
}
}
fn unchecked_eval<T>(&self, code: T) -> Value
where
T: AsRef<[u8]>,
{
let (mrb, ctx) = {
let borrow = self.borrow();
(borrow.mrb, borrow.ctx)
};
let context = {
let api = self.borrow();
if let Some(context) = api.context_stack.last() {
context.clone()
} else {
EvalContext::root()
}
};
if let Ok(cfilename) = CString::new(context.filename.to_owned()) {
unsafe {
sys::mrbc_filename(mrb, ctx, cfilename.as_ptr() as *const i8);
}
} else {
warn!("Could not set {} as mrc context filename", context.filename);
}
drop(context);
let args = Rc::new(Protect::new(self, code.as_ref()));
drop(code);
trace!("Evaling code on {}", mrb.debug());
let value = unsafe {
let data = sys::mrb_sys_cptr_value(mrb, Rc::into_raw(Rc::clone(&args)) as *mut c_void);
let mut state = <mem::MaybeUninit<sys::mrb_bool>>::uninit();
let value =
sys::mrb_protect(mrb, Some(Protect::run_protected), data, state.as_mut_ptr());
drop(args);
if state.assume_init() != 0 {
(*mrb).exc = sys::mrb_sys_obj_ptr(value);
sys::mrb_sys_raise_current_exception(mrb);
unreachable!("mrb_raise will unwind the stack with longjmp");
}
value
};
Value::new(self, value)
}
fn eval_with_context<T>(&self, code: T, context: EvalContext) -> Result<Value, MrbError>
where
T: AsRef<[u8]>,
{
self.push_context(context);
let result = self.eval(code.as_ref());
self.pop_context();
result
}
fn unchecked_eval_with_context<T>(&self, code: T, context: EvalContext) -> Value
where
T: AsRef<[u8]>,
{
self.push_context(context);
let result = self.unchecked_eval(code.as_ref());
self.pop_context();
result
}
fn peek_context(&self) -> Option<EvalContext> {
let api = self.borrow();
api.context_stack.last().cloned()
}
fn push_context(&self, context: EvalContext) {
let mut api = self.borrow_mut();
api.context_stack.push(context);
}
fn pop_context(&self) {
let mut api = self.borrow_mut();
api.context_stack.pop();
}
}
#[cfg(test)]
mod tests {
use crate::convert::{FromMrb, TryFromMrb};
use crate::def::{ClassLike, Define};
use crate::eval::{EvalContext, MrbEval};
use crate::file::MrbFile;
use crate::load::MrbLoadSources;
use crate::sys;
use crate::value::Value;
use crate::{Mrb, MrbError};
#[test]
fn root_eval_context() {
let interp = crate::interpreter().expect("mrb init");
let result = interp.eval("__FILE__").expect("eval");
let result = unsafe { String::try_from_mrb(&interp, result).expect("convert") };
assert_eq!(&result, "(eval)");
}
#[test]
fn context_is_restored_after_eval() {
let interp = crate::interpreter().expect("mrb init");
let context = EvalContext::new("context.rb");
interp.push_context(context);
interp.eval("15").expect("eval");
assert_eq!(interp.borrow().context_stack.len(), 1);
}
#[test]
fn root_context_is_not_pushed_after_eval() {
let interp = crate::interpreter().expect("mrb init");
interp.eval("15").expect("eval");
assert_eq!(interp.borrow().context_stack.len(), 0);
}
#[test]
#[should_panic]
fn eval_context_is_a_stack_for_nested_eval() {
struct NestedEval;
impl NestedEval {
unsafe extern "C" fn nested_eval(
mrb: *mut sys::mrb_state,
_slf: sys::mrb_value,
) -> sys::mrb_value {
let interp = unwrap_interpreter!(mrb);
if let Ok(value) = interp.eval("__FILE__") {
value.inner()
} else {
Value::from_mrb(&interp, None::<Value>).inner()
}
}
}
impl MrbFile for NestedEval {
fn require(interp: Mrb) -> Result<(), MrbError> {
let spec = {
let mut api = interp.borrow_mut();
let spec = api.def_module::<Self>("NestedEval", None);
spec.borrow_mut().add_self_method(
"file",
Self::nested_eval,
sys::mrb_args_none(),
);
spec
};
spec.borrow().define(&interp)?;
Ok(())
}
}
let interp = crate::interpreter().expect("mrb init");
interp
.def_file_for_type::<_, NestedEval>("nested_eval.rb")
.expect("def file");
let code = r#"
require 'nested_eval'
NestedEval.file
"#;
let result = interp.eval(code).expect("eval");
let result = unsafe { String::try_from_mrb(&interp, result).expect("convert") };
assert_eq!(&result, "/src/lib/nested_eval.rb");
}
#[test]
fn eval_with_context() {
let interp = crate::interpreter().expect("mrb init");
let result = interp
.eval_with_context("__FILE__", EvalContext::new("source.rb"))
.expect("eval");
let result = unsafe { String::try_from_mrb(&interp, result).expect("convert") };
assert_eq!(&result, "source.rb");
let result = interp
.eval_with_context("__FILE__", EvalContext::new("source.rb"))
.expect("eval");
let result = unsafe { String::try_from_mrb(&interp, result).expect("convert") };
assert_eq!(&result, "source.rb");
let result = interp
.eval_with_context("__FILE__", EvalContext::new("main.rb"))
.expect("eval");
let result = unsafe { String::try_from_mrb(&interp, result).expect("convert") };
assert_eq!(&result, "main.rb");
}
#[test]
fn unparseable_code_returns_err_syntax_error() {
let interp = crate::interpreter().expect("mrb init");
let result = interp.eval("'a").map(|_| ());
assert_eq!(
result,
Err(MrbError::Exec("SyntaxError: syntax error".to_owned()))
);
}
#[test]
fn interpreter_is_usable_after_syntax_error() {
let interp = crate::interpreter().expect("mrb init");
let result = interp.eval("'a").map(|_| ());
assert_eq!(
result,
Err(MrbError::Exec("SyntaxError: syntax error".to_owned()))
);
let result = interp.eval("'a' * 10 ").expect("eval");
let result = unsafe { String::try_from_mrb(&interp, result).expect("convert") };
assert_eq!(result, "a".repeat(10));
}
#[test]
fn file_magic_constant() {
let interp = crate::interpreter().expect("mrb init");
interp
.def_rb_source_file("source.rb", "def file; __FILE__; end")
.expect("def file");
let result = interp.eval("require 'source'; file").expect("eval");
let result = unsafe { String::try_from_mrb(&interp, result).expect("convert") };
assert_eq!(&result, "/src/lib/source.rb");
}
#[test]
fn file_not_persistent() {
let interp = crate::interpreter().expect("mrb init");
interp
.def_rb_source_file("source.rb", "def file; __FILE__; end")
.expect("def file");
let result = interp.eval("require 'source'; __FILE__").expect("eval");
let result = unsafe { String::try_from_mrb(&interp, result).expect("convert") };
assert_eq!(&result, "(eval)");
}
#[test]
fn return_syntax_error() {
let interp = crate::interpreter().expect("mrb init");
interp
.def_rb_source_file("fail.rb", "def bad; 'as'.scan(; end")
.expect("def file");
let result = interp.eval("require 'fail'").map(|_| ());
let expected = MrbError::Exec("SyntaxError: syntax error".to_owned());
assert_eq!(result, Err(expected));
}
}