rsjsonnet_front/
session.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use rsjsonnet_lang::arena::Arena;
5use rsjsonnet_lang::interner::InternedStr;
6use rsjsonnet_lang::program::{ImportError, LoadError, NativeError, Program, Thunk, Value};
7use rsjsonnet_lang::span::{SourceId, SpanId};
8
9use crate::src_manager::SrcManager;
10
11type NativeFunc<'p> = Box<dyn FnMut(&mut Program<'p>, &[Value<'p>]) -> Result<Value<'p>, String>>;
12
13pub struct Session<'p> {
14    program: Program<'p>,
15    inner: SessionInner<'p>,
16}
17
18struct SessionInner<'p> {
19    src_mgr: SrcManager,
20    source_paths: HashMap<SourceId, PathBuf>,
21    source_cache: HashMap<PathBuf, Thunk<'p>>,
22    search_paths: Vec<PathBuf>,
23    native_funcs: HashMap<InternedStr<'p>, NativeFunc<'p>>,
24    custom_stack_trace: Vec<String>,
25    max_trace: usize,
26    #[cfg(feature = "crossterm")]
27    colored_output: bool,
28}
29
30impl<'p> Session<'p> {
31    pub fn new(arena: &'p Arena) -> Self {
32        let program = Program::new(arena);
33        let (stdlib_src_id, stdlib_data) = program.get_stdlib_source();
34
35        let mut src_mgr = SrcManager::new();
36        src_mgr.insert_file(stdlib_src_id, "<stdlib>".into(), stdlib_data.into());
37
38        Self {
39            program,
40            inner: SessionInner {
41                src_mgr,
42                source_paths: HashMap::new(),
43                source_cache: HashMap::new(),
44                search_paths: Vec::new(),
45                native_funcs: HashMap::new(),
46                custom_stack_trace: Vec::new(),
47                max_trace: usize::MAX,
48                #[cfg(feature = "crossterm")]
49                colored_output: false,
50            },
51        }
52    }
53
54    /// Returns a reference to the underlying `Program`.
55    #[must_use]
56    #[inline]
57    pub fn program(&self) -> &Program<'p> {
58        &self.program
59    }
60
61    /// Returns a mutable reference to the underlying `Program`.
62    #[must_use]
63    #[inline]
64    pub fn program_mut(&mut self) -> &mut Program<'p> {
65        &mut self.program
66    }
67
68    /// Sets the maximum number of stack trace items to print.
69    pub fn set_max_trace(&mut self, max_trace: usize) {
70        self.inner.max_trace = max_trace;
71    }
72
73    #[cfg(feature = "crossterm")]
74    pub fn set_colored_output(&mut self, colored_output: bool) {
75        self.inner.colored_output = colored_output;
76    }
77
78    pub fn add_search_path(&mut self, path: PathBuf) {
79        self.inner.search_paths.push(path);
80    }
81
82    /// Adds a native function.
83    ///
84    /// # Example
85    ///
86    /// ```
87    /// let arena = rsjsonnet_lang::arena::Arena::new();
88    /// let mut session = rsjsonnet_front::Session::new(&arena);
89    ///
90    /// session.add_native_func("MyFunc", &["arg"], |_, [arg]| {
91    ///     let Some(arg) = arg.to_string() else {
92    ///         return Err("expected a string".into());
93    ///     };
94    ///
95    ///     // Count lowercase vowels.
96    ///     let r = arg
97    ///         .chars()
98    ///         .filter(|chr| matches!(chr, 'a' | 'e' | 'i' | 'o' | 'u'))
99    ///         .count();
100    ///     Ok(rsjsonnet_lang::program::Value::number(r as f64))
101    /// });
102    ///
103    /// let source = br#"local f = std.native("MyFunc"); f("hello world")"#;
104    /// let thunk = session
105    ///     .load_virt_file("<example>", source.to_vec())
106    ///     .unwrap();
107    ///
108    /// let result = session.eval_value(&thunk).unwrap();
109    ///
110    /// assert_eq!(result.as_number(), Some(3.0));
111    /// ```
112    pub fn add_native_func<const N: usize, F>(
113        &mut self,
114        name: &str,
115        params: &[&str; N],
116        mut func: F,
117    ) where
118        F: FnMut(&mut Program<'p>, &[Value<'p>; N]) -> Result<Value<'p>, String> + 'static,
119    {
120        let name = self.program.intern_str(name);
121        let params: Vec<_> = params.iter().map(|p| self.program.intern_str(p)).collect();
122
123        self.program.register_native_func(name, &params);
124        self.inner.native_funcs.insert(
125            name,
126            Box::new(move |program, args| func(program, args.try_into().unwrap())),
127        );
128    }
129
130    /// Loads a file with the provided `data`.
131    ///
132    /// `repr_path` is used to represent the file in error messages and for
133    /// `std.thisFile`.
134    ///
135    /// In case of failure, the error is printed to stderr and `None` is
136    /// returned.
137    pub fn load_virt_file(&mut self, repr_path: &str, data: Vec<u8>) -> Option<Thunk<'p>> {
138        self.inner
139            .load_virt_file(&mut self.program, repr_path, data)
140    }
141
142    /// Loads a file from the filesystem.
143    ///
144    /// In case of failure, the error is printed to stderr and `None` is
145    /// returned.
146    pub fn load_real_file(&mut self, path: &Path) -> Option<Thunk<'p>> {
147        self.inner.load_real_file(&mut self.program, path)
148    }
149
150    /// Evaluates a thunk.
151    ///
152    /// In case of failure, the error is printed to stderr and `None` is
153    /// returned.
154    pub fn eval_value(&mut self, thunk: &Thunk<'p>) -> Option<Value<'p>> {
155        match self.program.eval_value(thunk, &mut self.inner) {
156            Ok(v) => Some(v),
157            Err(e) => {
158                self.inner.print_eval_error(&self.program, &e);
159                None
160            }
161        }
162    }
163
164    /// Evaluates a function call.
165    ///
166    /// In case of failure, the error is printed to stderr and `None` is
167    /// returned.
168    pub fn eval_call(
169        &mut self,
170        func: &Thunk<'p>,
171        pos_args: &[Thunk<'p>],
172        named_args: &[(InternedStr<'p>, Thunk<'p>)],
173    ) -> Option<Value<'p>> {
174        match self
175            .program
176            .eval_call(func, pos_args, named_args, &mut self.inner)
177        {
178            Ok(v) => Some(v),
179            Err(e) => {
180                self.inner.print_eval_error(&self.program, &e);
181                None
182            }
183        }
184    }
185
186    /// Marshals a value as JSON.
187    ///
188    /// In case of failure, the error is printed to stderr and `None` is
189    /// returned.
190    pub fn manifest_json(&mut self, value: &Value<'p>, multiline: bool) -> Option<String> {
191        match self.program.manifest_json(value, multiline) {
192            Ok(s) => Some(s),
193            Err(e) => {
194                self.inner.print_eval_error(&self.program, &e);
195                None
196            }
197        }
198    }
199
200    pub fn print_error(&self, msg: &str) {
201        self.inner.print_error(msg);
202    }
203
204    pub fn print_note(&self, msg: &str) {
205        self.inner.print_note(msg);
206    }
207
208    /// Pushes a custom item that will be included at the end of stack straces.
209    pub fn push_custom_stack_trace_item(&mut self, text: String) {
210        self.inner.custom_stack_trace.push(text);
211    }
212
213    /// Removes the last item pushed with
214    /// [`Session::push_custom_stack_trace_item`].
215    pub fn pop_custom_stack_trace_item(&mut self) {
216        self.inner.custom_stack_trace.pop().unwrap();
217    }
218}
219
220impl<'p> SessionInner<'p> {
221    fn load_virt_file(
222        &mut self,
223        program: &mut Program<'p>,
224        repr_path: &str,
225        data: Vec<u8>,
226    ) -> Option<Thunk<'p>> {
227        let (span_ctx, source_id) = program.span_manager_mut().insert_source_context(data.len());
228
229        self.src_mgr
230            .insert_file(source_id, repr_path.into(), data.into_boxed_slice());
231        let data = self.src_mgr.get_file_data(source_id);
232
233        match program.load_source(span_ctx, data, true, repr_path) {
234            Ok(thunk) => Some(thunk),
235            Err(ref e) => {
236                self.print_load_error(program, e);
237                None
238            }
239        }
240    }
241
242    fn load_real_file(&mut self, program: &mut Program<'p>, path: &Path) -> Option<Thunk<'p>> {
243        let norm_path = match path.canonicalize() {
244            Ok(p) => p,
245            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
246                self.print_error(format_args!("file {path:?} does not exist"));
247                return None;
248            }
249            Err(e) => {
250                self.print_error(format_args!("failed to canonicalize path {path:?}: {e}"));
251                return None;
252            }
253        };
254        if let Some(thunk) = self.source_cache.get(&norm_path) {
255            return Some(thunk.clone());
256        }
257
258        let data = match std::fs::read(path) {
259            Ok(data) => data,
260            Err(e) => {
261                self.print_error(format_args!("failed to read {path:?}: {e}"));
262                return None;
263            }
264        };
265
266        let repr_path = path.display().to_string();
267
268        let (span_ctx, source_id) = program.span_manager_mut().insert_source_context(data.len());
269        self.src_mgr
270            .insert_file(source_id, repr_path.clone(), data.into_boxed_slice());
271        self.source_paths.insert(source_id, path.to_path_buf());
272        let data = self.src_mgr.get_file_data(source_id);
273
274        match program.load_source(span_ctx, data, true, &repr_path) {
275            Ok(thunk) => {
276                self.source_cache.insert(norm_path, thunk.clone());
277                Some(thunk)
278            }
279            Err(ref e) => {
280                self.print_load_error(program, e);
281                None
282            }
283        }
284    }
285
286    fn find_import(&self, program: &Program<'p>, from: SpanId, path: &str) -> Option<PathBuf> {
287        let path = Path::new(path);
288        if path.is_absolute() {
289            if path.exists() {
290                Some(path.to_path_buf())
291            } else {
292                None
293            }
294        } else {
295            let (from_ctx, _, _) = program.span_manager().get_span(from);
296            let from_dir_path = match *program.span_manager().get_context(from_ctx) {
297                rsjsonnet_lang::span::SpanContext::Source(from_src) => {
298                    self.source_paths.get(&from_src).and_then(|p| p.parent())
299                }
300            };
301
302            for base_path in from_dir_path
303                .into_iter()
304                .chain(self.search_paths.iter().map(PathBuf::as_path))
305            {
306                let full_path = base_path.join(path);
307                if full_path.exists() {
308                    return Some(full_path);
309                }
310            }
311            None
312        }
313    }
314
315    fn print_rich_message(&self, msg: &[(String, crate::print::TextPartKind)]) {
316        #[cfg(feature = "crossterm")]
317        if self.colored_output {
318            crate::print::output_stderr_colored(msg);
319            return;
320        }
321
322        crate::print::output_stderr_plain(msg);
323    }
324
325    fn print_load_error(&self, program: &Program<'p>, error: &LoadError) {
326        match error {
327            LoadError::Lex(e) => {
328                let msg =
329                    crate::report::lexer::render_error(e, program.span_manager(), &self.src_mgr);
330                self.print_rich_message(&msg);
331            }
332            LoadError::Parse(e) => {
333                let msg =
334                    crate::report::parser::render_error(e, program.span_manager(), &self.src_mgr);
335                self.print_rich_message(&msg);
336            }
337            LoadError::Analyze(e) => {
338                let msg =
339                    crate::report::analyze::render_error(e, program.span_manager(), &self.src_mgr);
340                self.print_rich_message(&msg);
341            }
342        }
343    }
344
345    fn print_eval_error(&self, program: &Program<'p>, error: &rsjsonnet_lang::program::EvalError) {
346        self.print_rich_message(&crate::report::eval::render_error_kind(
347            &error.kind,
348            program.span_manager(),
349            &self.src_mgr,
350        ));
351        self.print_stack_trace(program, &error.stack_trace);
352        eprintln!();
353    }
354
355    fn print_error<T: std::fmt::Display>(&self, msg: T) {
356        let msg = crate::report::render_error_message(msg);
357        self.print_rich_message(&msg);
358    }
359
360    fn print_note<T: std::fmt::Display>(&self, msg: T) {
361        let msg = crate::report::render_note_message(msg);
362        self.print_rich_message(&msg);
363    }
364
365    fn print_stack_trace(
366        &self,
367        program: &Program<'p>,
368        stack: &[rsjsonnet_lang::program::EvalStackTraceItem],
369    ) {
370        if stack.len() <= self.max_trace {
371            self.print_rich_message(&crate::report::stack_trace::render(
372                stack,
373                program.span_manager(),
374                &self.src_mgr,
375            ));
376        } else {
377            let second_len = self.max_trace / 2;
378            let first_len = self.max_trace - second_len;
379
380            self.print_rich_message(&crate::report::stack_trace::render(
381                &stack[(stack.len() - first_len)..],
382                program.span_manager(),
383                &self.src_mgr,
384            ));
385            self.print_note(format_args!(
386                "... {} items hidden ...",
387                stack.len() - self.max_trace
388            ));
389            self.print_rich_message(&crate::report::stack_trace::render(
390                &stack[..second_len],
391                program.span_manager(),
392                &self.src_mgr,
393            ));
394        }
395
396        for custom_item in self.custom_stack_trace.iter().rev() {
397            self.print_note(custom_item);
398        }
399    }
400}
401
402impl<'p> rsjsonnet_lang::program::Callbacks<'p> for SessionInner<'p> {
403    fn import(
404        &mut self,
405        program: &mut Program<'p>,
406        from: SpanId,
407        path: &str,
408    ) -> Result<Thunk<'p>, ImportError> {
409        let Some(full_path) = self.find_import(program, from, path) else {
410            self.print_error(format_args!("import {path:?} not found in search path"));
411            return Err(ImportError);
412        };
413        match self.load_real_file(program, &full_path) {
414            None => Err(ImportError),
415            Some(thunk) => Ok(thunk),
416        }
417    }
418
419    fn import_str(
420        &mut self,
421        program: &mut Program<'p>,
422        from: SpanId,
423        path: &str,
424    ) -> Result<String, ImportError> {
425        let Some(full_path) = self.find_import(program, from, path) else {
426            self.print_error(format_args!("import {path:?} not found in search path"));
427            return Err(ImportError);
428        };
429        let data = match std::fs::read(&full_path) {
430            Ok(data) => data,
431            Err(e) => {
432                self.print_error(format_args!("failed to read {full_path:?}: {e}"));
433                return Err(ImportError);
434            }
435        };
436        Ok(String::from_utf8_lossy(&data).into_owned())
437    }
438
439    fn import_bin(
440        &mut self,
441        program: &mut Program<'p>,
442        from: SpanId,
443        path: &str,
444    ) -> Result<Vec<u8>, ImportError> {
445        let Some(full_path) = self.find_import(program, from, path) else {
446            self.print_error(format_args!("import {path:?} not found in search path"));
447            return Err(ImportError);
448        };
449        let data = match std::fs::read(&full_path) {
450            Ok(data) => data,
451            Err(e) => {
452                self.print_error(format_args!("failed to read {full_path:?}: {e}"));
453                return Err(ImportError);
454            }
455        };
456        Ok(data)
457    }
458
459    fn trace(
460        &mut self,
461        program: &mut Program<'p>,
462        message: &str,
463        stack: &[rsjsonnet_lang::program::EvalStackTraceItem],
464    ) {
465        self.print_rich_message(&[
466            ("TRACE".into(), crate::print::TextPartKind::NoteLabel),
467            (": ".into(), crate::print::TextPartKind::MainMessage),
468            (message.into(), crate::print::TextPartKind::MainMessage),
469            ('\n'.into(), crate::print::TextPartKind::Space),
470        ]);
471        self.print_stack_trace(program, stack);
472        eprintln!();
473    }
474
475    fn native_call(
476        &mut self,
477        program: &mut Program<'p>,
478        name: InternedStr<'p>,
479        args: &[Value<'p>],
480    ) -> Result<Value<'p>, NativeError> {
481        let native_func = &mut self.native_funcs.get_mut(&name).unwrap();
482        match native_func(program, args) {
483            Ok(v) => Ok(v),
484            Err(e) => {
485                self.print_error(format_args!("native function {name:?} failed: {e}"));
486                Err(NativeError)
487            }
488        }
489    }
490}