artichoke_backend/state/
parser.rs

1use std::borrow::Cow;
2use std::ffi::{CStr, CString};
3use std::ptr::NonNull;
4
5use crate::core::IncrementLinenoError;
6use crate::sys;
7
8/// Filename of the top eval context.
9pub const TOP_FILENAME: &[u8] = b"(eval)";
10
11#[derive(Debug)]
12pub struct State {
13    context: NonNull<sys::mrbc_context>,
14    stack: Vec<Context>,
15}
16
17impl State {
18    pub fn new(mrb: &mut sys::mrb_state) -> Option<Self> {
19        let context = unsafe { sys::mrbc_context_new(mrb) };
20        let mut context = NonNull::new(context)?;
21        reset_context_filename(mrb, unsafe { context.as_mut() });
22        Some(Self { context, stack: vec![] })
23    }
24
25    pub fn close(mut self, mrb: &mut sys::mrb_state) {
26        unsafe {
27            let ctx = self.context.as_mut();
28            sys::mrbc_context_free(mrb, ctx);
29        }
30    }
31
32    pub fn context_mut(&mut self) -> &mut sys::mrbc_context {
33        unsafe { self.context.as_mut() }
34    }
35
36    /// Reset line number to `1`.
37    pub fn reset(&mut self, mrb: &mut sys::mrb_state) {
38        unsafe {
39            let ctx = self.context.as_mut();
40            ctx.lineno = 1;
41            reset_context_filename(mrb, ctx);
42        }
43        self.stack.clear();
44    }
45
46    /// Fetch the current line number from the parser state.
47    #[must_use]
48    pub fn fetch_lineno(&self) -> usize {
49        let ctx = unsafe { self.context.as_ref() };
50        usize::from(ctx.lineno)
51    }
52
53    /// Increment line number and return the new value.
54    ///
55    /// # Errors
56    ///
57    /// This function returns [`IncrementLinenoError`] if the increment results
58    /// in an overflow of the internal parser line number counter.
59    pub fn add_fetch_lineno(&mut self, val: usize) -> Result<usize, IncrementLinenoError> {
60        let old = usize::from(unsafe { self.context.as_ref() }.lineno);
61        let new = old
62            .checked_add(val)
63            .ok_or_else(|| IncrementLinenoError::Overflow(usize::from(u16::MAX)))?;
64        let store = u16::try_from(new).map_err(|_| IncrementLinenoError::Overflow(usize::from(u16::MAX)))?;
65        unsafe {
66            self.context.as_mut().lineno = store;
67        }
68        Ok(new)
69    }
70
71    /// Push a [`Context`] onto the stack.
72    ///
73    /// The supplied [`Context`] becomes the currently active context. This
74    /// function modifies the parser state so subsequently `eval`ed code will
75    /// use the current active `Context`.
76    pub fn push_context(&mut self, mrb: &mut sys::mrb_state, context: Context) {
77        let filename = context.filename_as_c_str();
78        unsafe {
79            let ctx = self.context.as_mut();
80            sys::mrbc_filename(mrb, ctx, filename.as_ptr());
81        }
82        self.stack.push(context);
83    }
84
85    /// Removes the last element from the context stack and returns it, or
86    /// `None` if the stack is empty.
87    ///
88    /// Calls to this function modify the parser state so subsequently `eval`ed
89    /// code will use the current active [`Context`].
90    pub fn pop_context(&mut self, mrb: &mut sys::mrb_state) -> Option<Context> {
91        let context = self.stack.pop();
92        if let Some(current) = self.stack.last() {
93            let filename = current.filename_as_c_str();
94            unsafe {
95                let ctx = self.context.as_mut();
96                sys::mrbc_filename(mrb, ctx, filename.as_ptr());
97            }
98        } else {
99            unsafe {
100                let ctx = self.context.as_mut();
101                reset_context_filename(mrb, ctx);
102            }
103        }
104        context
105    }
106
107    /// Returns the last [`Context`], or `None` if the context stack is empty.
108    #[must_use]
109    pub fn peek_context(&self) -> Option<&Context> {
110        self.stack.last()
111    }
112}
113
114fn reset_context_filename(mrb: &mut sys::mrb_state, context: &mut sys::mrbc_context) {
115    let frame = Context::root();
116    let filename = frame.filename_as_c_str();
117    unsafe {
118        sys::mrbc_filename(mrb, context, filename.as_ptr());
119    }
120}
121
122/// `Context` is used to manipulate the current filename on the parser.
123///
124/// Parser [`State`] maintains a stack of `Context`s and [`eval`] uses the
125/// `Context` stack to set the `__FILE__` magic constant.
126///
127/// [`eval`]: crate::core::Eval
128#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
129pub struct Context {
130    /// Value of the `__FILE__` magic constant that also appears in stack
131    /// frames.
132    filename: Cow<'static, [u8]>,
133    /// FFI NUL-terminated C string variant of `filename` field.
134    filename_cstr: Box<CStr>,
135}
136
137impl Default for Context {
138    fn default() -> Self {
139        // SAFETY: `TOP_FILENAME` has no NUL bytes (asserted by tests).
140        unsafe { Self::new_unchecked(TOP_FILENAME) }
141    }
142}
143
144impl Context {
145    /// Create a new [`Context`].
146    pub fn new<T>(filename: T) -> Option<Self>
147    where
148        T: Into<Cow<'static, [u8]>>,
149    {
150        let filename = filename.into();
151        let cstring = CString::new(filename.clone()).ok()?;
152        Some(Self {
153            filename,
154            filename_cstr: cstring.into_boxed_c_str(),
155        })
156    }
157
158    /// Create a new [`Context`] without checking for NUL bytes in the filename.
159    ///
160    /// # Safety
161    ///
162    /// `filename` must not contain any NUL bytes. `filename` must not contain a
163    /// trailing `NUL`.
164    pub unsafe fn new_unchecked<T>(filename: T) -> Self
165    where
166        T: Into<Cow<'static, [u8]>>,
167    {
168        let filename = filename.into();
169        // SAFETY: callers must guarantee that `filename` is a valid C string.
170        let cstring = unsafe { CString::from_vec_unchecked(filename.clone().into_owned()) };
171        Self {
172            filename,
173            filename_cstr: cstring.into_boxed_c_str(),
174        }
175    }
176
177    /// Create a root, or default, [`Context`].
178    ///
179    /// The root context sets the `__FILE__` magic constant to "(eval)".
180    #[must_use]
181    pub fn root() -> Self {
182        Self::default()
183    }
184
185    /// Filename of this `Context`.
186    #[must_use]
187    pub fn filename(&self) -> &[u8] {
188        &self.filename
189    }
190
191    /// FFI-safe NUL-terminated C String of this `Context`.
192    ///
193    /// This [`CStr`] is valid as long as this `Context` is not dropped.
194    #[must_use]
195    pub fn filename_as_c_str(&self) -> &CStr {
196        &self.filename_cstr
197    }
198}
199
200#[cfg(test)]
201mod test {
202    use super::Context;
203
204    #[test]
205    fn top_filename_does_not_contain_nul_byte() {
206        let contains_nul_byte = super::TOP_FILENAME.iter().copied().any(|b| b == b'\0');
207        assert!(!contains_nul_byte);
208    }
209
210    #[test]
211    fn top_filename_context_new_unchecked_safety() {
212        Context::new(super::TOP_FILENAME).unwrap();
213    }
214}