1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
use std::borrow::Cow;
use std::ffi::{CStr, CString};
use std::ptr::NonNull;

use crate::core::IncrementLinenoError;
use crate::sys;

/// Filename of the top eval context.
pub const TOP_FILENAME: &[u8] = b"(eval)";

#[derive(Debug)]
pub struct State {
    context: NonNull<sys::mrbc_context>,
    stack: Vec<Context>,
}

impl State {
    pub fn new(mrb: &mut sys::mrb_state) -> Option<Self> {
        let context = unsafe { sys::mrbc_context_new(mrb) };
        let mut context = NonNull::new(context)?;
        reset_context_filename(mrb, unsafe { context.as_mut() });
        Some(Self { context, stack: vec![] })
    }

    pub fn close(mut self, mrb: &mut sys::mrb_state) {
        unsafe {
            let ctx = self.context.as_mut();
            sys::mrbc_context_free(mrb, ctx);
        }
    }

    pub fn context_mut(&mut self) -> &mut sys::mrbc_context {
        unsafe { self.context.as_mut() }
    }

    /// Reset line number to `1`.
    pub fn reset(&mut self, mrb: &mut sys::mrb_state) {
        unsafe {
            let ctx = self.context.as_mut();
            ctx.lineno = 1;
            reset_context_filename(mrb, ctx);
        }
        self.stack.clear();
    }

    /// Fetch the current line number from the parser state.
    #[must_use]
    pub fn fetch_lineno(&self) -> usize {
        let ctx = unsafe { self.context.as_ref() };
        usize::from(ctx.lineno)
    }

    /// Increment line number and return the new value.
    ///
    /// # Errors
    ///
    /// This function returns [`IncrementLinenoError`] if the increment results
    /// in an overflow of the internal parser line number counter.
    pub fn add_fetch_lineno(&mut self, val: usize) -> Result<usize, IncrementLinenoError> {
        let old = usize::from(unsafe { self.context.as_ref() }.lineno);
        let new = old
            .checked_add(val)
            .ok_or_else(|| IncrementLinenoError::Overflow(usize::from(u16::MAX)))?;
        let store = u16::try_from(new).map_err(|_| IncrementLinenoError::Overflow(usize::from(u16::MAX)))?;
        unsafe {
            self.context.as_mut().lineno = store;
        }
        Ok(new)
    }

    /// Push a [`Context`] onto the stack.
    ///
    /// The supplied [`Context`] becomes the currently active context. This
    /// function modifies the parser state so subsequently `eval`ed code will
    /// use the current active `Context`.
    pub fn push_context(&mut self, mrb: &mut sys::mrb_state, context: Context) {
        let filename = context.filename_as_c_str();
        unsafe {
            let ctx = self.context.as_mut();
            sys::mrbc_filename(mrb, ctx, filename.as_ptr());
        }
        self.stack.push(context);
    }

    /// Removes the last element from the context stack and returns it, or
    /// `None` if the stack is empty.
    ///
    /// Calls to this function modify the parser state so subsequently `eval`ed
    /// code will use the current active [`Context`].
    pub fn pop_context(&mut self, mrb: &mut sys::mrb_state) -> Option<Context> {
        let context = self.stack.pop();
        if let Some(current) = self.stack.last() {
            let filename = current.filename_as_c_str();
            unsafe {
                let ctx = self.context.as_mut();
                sys::mrbc_filename(mrb, ctx, filename.as_ptr());
            }
        } else {
            unsafe {
                let ctx = self.context.as_mut();
                reset_context_filename(mrb, ctx);
            }
        }
        context
    }

    /// Returns the last [`Context`], or `None` if the context stack is empty.
    #[must_use]
    pub fn peek_context(&self) -> Option<&Context> {
        self.stack.last()
    }
}

fn reset_context_filename(mrb: &mut sys::mrb_state, context: &mut sys::mrbc_context) {
    let frame = Context::root();
    let filename = frame.filename_as_c_str();
    unsafe {
        sys::mrbc_filename(mrb, context, filename.as_ptr());
    }
}

/// `Context` is used to manipulate the current filename on the parser.
///
/// Parser [`State`] maintains a stack of `Context`s and [`eval`] uses the
/// `Context` stack to set the `__FILE__` magic constant.
///
/// [`eval`]: crate::core::Eval
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Context {
    /// Value of the `__FILE__` magic constant that also appears in stack
    /// frames.
    filename: Cow<'static, [u8]>,
    /// FFI NUL-terminated C string variant of `filename` field.
    filename_cstr: Box<CStr>,
}

impl Default for Context {
    fn default() -> Self {
        // SAFETY: `TOP_FILENAME` has no NUL bytes (asserted by tests).
        unsafe { Self::new_unchecked(TOP_FILENAME) }
    }
}

impl Context {
    /// Create a new [`Context`].
    pub fn new<T>(filename: T) -> Option<Self>
    where
        T: Into<Cow<'static, [u8]>>,
    {
        let filename = filename.into();
        let cstring = CString::new(filename.clone()).ok()?;
        Some(Self {
            filename,
            filename_cstr: cstring.into_boxed_c_str(),
        })
    }

    /// Create a new [`Context`] without checking for NUL bytes in the filename.
    ///
    /// # Safety
    ///
    /// `filename` must not contain any NUL bytes. `filename` must not contain a
    /// trailing `NUL`.
    pub unsafe fn new_unchecked<T>(filename: T) -> Self
    where
        T: Into<Cow<'static, [u8]>>,
    {
        let filename = filename.into();
        let cstring = CString::from_vec_unchecked(filename.clone().into_owned());
        Self {
            filename,
            filename_cstr: cstring.into_boxed_c_str(),
        }
    }

    /// Create a root, or default, [`Context`].
    ///
    /// The root context sets the `__FILE__` magic constant to "(eval)".
    #[must_use]
    pub fn root() -> Self {
        Self::default()
    }

    /// Filename of this `Context`.
    #[must_use]
    pub fn filename(&self) -> &[u8] {
        &self.filename
    }

    /// FFI-safe NUL-terminated C String of this `Context`.
    ///
    /// This [`CStr`] is valid as long as this `Context` is not dropped.
    #[must_use]
    pub fn filename_as_c_str(&self) -> &CStr {
        &self.filename_cstr
    }
}

#[cfg(test)]
mod test {
    use super::Context;

    #[test]
    fn top_filename_does_not_contain_nul_byte() {
        let contains_nul_byte = super::TOP_FILENAME.iter().copied().any(|b| b == b'\0');
        assert!(!contains_nul_byte);
    }

    #[test]
    fn top_filename_context_new_unchecked_safety() {
        Context::new(super::TOP_FILENAME).unwrap();
    }
}