solar_interface/
session.rs

1use crate::{
2    ColorChoice, SessionGlobals, SourceMap,
3    diagnostics::{DiagCtxt, EmittedDiagnostics},
4};
5use solar_config::{CompilerOutput, CompilerStage, Opts, SINGLE_THREADED_TARGET, UnstableOpts};
6use std::{path::Path, sync::Arc};
7
8/// Information about the current compiler session.
9pub struct Session {
10    /// The compiler options.
11    pub opts: Opts,
12
13    /// The diagnostics context.
14    pub dcx: DiagCtxt,
15    /// The globals.
16    globals: SessionGlobals,
17}
18
19/// [`Session`] builder.
20#[derive(Default)]
21#[must_use = "builders don't do anything unless you call `build`"]
22pub struct SessionBuilder {
23    dcx: Option<DiagCtxt>,
24    globals: Option<SessionGlobals>,
25    opts: Option<Opts>,
26}
27
28impl SessionBuilder {
29    /// Sets the diagnostic context. This is required.
30    ///
31    /// See also the `with_*_emitter*` methods.
32    pub fn dcx(mut self, dcx: DiagCtxt) -> Self {
33        self.dcx = Some(dcx);
34        self
35    }
36
37    /// Sets the source map.
38    pub fn source_map(mut self, source_map: Arc<SourceMap>) -> Self {
39        self.get_globals().source_map = source_map;
40        self
41    }
42
43    /// Sets the compiler options.
44    pub fn opts(mut self, opts: Opts) -> Self {
45        self.opts = Some(opts);
46        self
47    }
48
49    /// Sets the diagnostic context to a test emitter.
50    #[inline]
51    pub fn with_test_emitter(mut self) -> Self {
52        let sm = self.get_source_map();
53        self.dcx(DiagCtxt::with_test_emitter(Some(sm)))
54    }
55
56    /// Sets the diagnostic context to a stderr emitter.
57    #[inline]
58    pub fn with_stderr_emitter(self) -> Self {
59        self.with_stderr_emitter_and_color(ColorChoice::Auto)
60    }
61
62    /// Sets the diagnostic context to a stderr emitter and a color choice.
63    #[inline]
64    pub fn with_stderr_emitter_and_color(mut self, color_choice: ColorChoice) -> Self {
65        let sm = self.get_source_map();
66        self.dcx(DiagCtxt::with_stderr_emitter_and_color(Some(sm), color_choice))
67    }
68
69    /// Sets the diagnostic context to a human emitter that emits diagnostics to a local buffer.
70    #[inline]
71    pub fn with_buffer_emitter(mut self, color_choice: ColorChoice) -> Self {
72        let sm = self.get_source_map();
73        self.dcx(DiagCtxt::with_buffer_emitter(Some(sm), color_choice))
74    }
75
76    /// Sets the diagnostic context to a silent emitter.
77    #[inline]
78    pub fn with_silent_emitter(self, fatal_note: Option<String>) -> Self {
79        self.dcx(DiagCtxt::with_silent_emitter(fatal_note))
80    }
81
82    /// Sets the number of threads to use for parallelism to 1.
83    #[inline]
84    pub fn single_threaded(self) -> Self {
85        self.threads(1)
86    }
87
88    /// Sets the number of threads to use for parallelism. Zero specifies the number of logical
89    /// cores.
90    #[inline]
91    pub fn threads(mut self, threads: usize) -> Self {
92        self.opts_mut().threads = threads.into();
93        self
94    }
95
96    /// Gets the source map from the diagnostics context.
97    fn get_source_map(&mut self) -> Arc<SourceMap> {
98        self.get_globals().source_map.clone()
99    }
100
101    fn get_globals(&mut self) -> &mut SessionGlobals {
102        self.globals.get_or_insert_default()
103    }
104
105    /// Returns a mutable reference to the options.
106    fn opts_mut(&mut self) -> &mut Opts {
107        self.opts.get_or_insert_default()
108    }
109
110    /// Consumes the builder to create a new session.
111    ///
112    /// The diagnostics context must be set before calling this method, either by calling
113    /// [`dcx`](Self::dcx) or by using one of the provided helper methods, like
114    /// [`with_stderr_emitter`](Self::with_stderr_emitter).
115    ///
116    /// # Panics
117    ///
118    /// Panics if:
119    /// - the diagnostics context is not set
120    /// - the source map in the diagnostics context does not match the one set in the builder
121    #[track_caller]
122    pub fn build(mut self) -> Session {
123        let mut dcx = self.dcx.take().unwrap_or_else(|| panic!("diagnostics context not set"));
124        Session {
125            globals: match self.globals.take() {
126                Some(globals) => {
127                    // Check that the source map matches the one in the diagnostics context.
128                    if let Some(sm) = dcx.source_map_mut() {
129                        assert!(
130                            Arc::ptr_eq(&globals.source_map, sm),
131                            "session source map does not match the one in the diagnostics context"
132                        );
133                    }
134                    globals
135                }
136                None => {
137                    // Set the source map from the diagnostics context.
138                    let sm = dcx.source_map_mut().cloned().unwrap_or_default();
139                    SessionGlobals::new(sm)
140                }
141            },
142            dcx,
143            opts: self.opts.take().unwrap_or_default(),
144        }
145    }
146}
147
148impl Session {
149    /// Creates a new session with the given diagnostics context and source map.
150    pub fn new(dcx: DiagCtxt, source_map: Arc<SourceMap>) -> Self {
151        Self::builder().dcx(dcx).source_map(source_map).build()
152    }
153
154    /// Creates a new session with the given diagnostics context and an empty source map.
155    pub fn empty(dcx: DiagCtxt) -> Self {
156        Self::builder().dcx(dcx).build()
157    }
158
159    /// Creates a new session builder.
160    #[inline]
161    pub fn builder() -> SessionBuilder {
162        SessionBuilder::default()
163    }
164
165    /// Infers the language from the input files.
166    pub fn infer_language(&mut self) {
167        if !self.opts.input.is_empty()
168            && self.opts.input.iter().all(|arg| Path::new(arg).extension() == Some("yul".as_ref()))
169        {
170            self.opts.language = solar_config::Language::Yul;
171        }
172    }
173
174    /// Validates the session options.
175    pub fn validate(&self) -> crate::Result<()> {
176        let mut result = Ok(());
177        result = result.and(self.check_unique("emit", &self.opts.emit));
178        result
179    }
180
181    fn check_unique<T: Eq + std::hash::Hash + std::fmt::Display>(
182        &self,
183        name: &str,
184        list: &[T],
185    ) -> crate::Result<()> {
186        let mut result = Ok(());
187        let mut seen = std::collections::HashSet::new();
188        for item in list {
189            if !seen.insert(item) {
190                let msg = format!("cannot specify `--{name} {item}` twice");
191                result = Err(self.dcx.err(msg).emit());
192            }
193        }
194        result
195    }
196
197    /// Returns the unstable options.
198    #[inline]
199    pub fn unstable(&self) -> &UnstableOpts {
200        &self.opts.unstable
201    }
202
203    /// Returns the emitted diagnostics. Can be empty.
204    ///
205    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
206    /// [`with_buffer_emitter`](SessionBuilder::with_buffer_emitter).
207    #[inline]
208    pub fn emitted_diagnostics(&self) -> Option<EmittedDiagnostics> {
209        self.dcx.emitted_diagnostics()
210    }
211
212    /// Returns `Err` with the printed diagnostics if any errors have been emitted.
213    ///
214    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
215    /// [`with_buffer_emitter`](SessionBuilder::with_buffer_emitter).
216    #[inline]
217    pub fn emitted_errors(&self) -> Option<Result<(), EmittedDiagnostics>> {
218        self.dcx.emitted_errors()
219    }
220
221    /// Returns a reference to the source map.
222    #[inline]
223    pub fn source_map(&self) -> &SourceMap {
224        &self.globals.source_map
225    }
226
227    /// Clones the source map.
228    #[inline]
229    pub fn clone_source_map(&self) -> Arc<SourceMap> {
230        self.globals.source_map.clone()
231    }
232
233    /// Returns `true` if compilation should stop after the given stage.
234    #[inline]
235    pub fn stop_after(&self, stage: CompilerStage) -> bool {
236        self.opts.stop_after >= Some(stage)
237    }
238
239    /// Returns the number of threads to use for parallelism.
240    #[inline]
241    pub fn threads(&self) -> usize {
242        self.opts.threads().get()
243    }
244
245    /// Returns `true` if parallelism is not enabled.
246    #[inline]
247    pub fn is_sequential(&self) -> bool {
248        self.threads() == 1
249    }
250
251    /// Returns `true` if parallelism is enabled.
252    #[inline]
253    pub fn is_parallel(&self) -> bool {
254        !self.is_sequential()
255    }
256
257    /// Returns `true` if the given output should be emitted.
258    #[inline]
259    pub fn do_emit(&self, output: CompilerOutput) -> bool {
260        self.opts.emit.contains(&output)
261    }
262
263    /// Spawns the given closure on the thread pool or executes it immediately if parallelism is not
264    /// enabled.
265    // NOTE: This only exists because on a `use_current_thread` thread pool `rayon::spawn` will
266    // never execute.
267    #[inline]
268    pub fn spawn(&self, f: impl FnOnce() + Send + 'static) {
269        if self.is_sequential() {
270            f();
271        } else {
272            rayon::spawn(f);
273        }
274    }
275
276    /// Takes two closures and potentially runs them in parallel. It returns a pair of the results
277    /// from those closures.
278    #[inline]
279    pub fn join<A, B, RA, RB>(&self, oper_a: A, oper_b: B) -> (RA, RB)
280    where
281        A: FnOnce() -> RA + Send,
282        B: FnOnce() -> RB + Send,
283        RA: Send,
284        RB: Send,
285    {
286        if self.is_sequential() { (oper_a(), oper_b()) } else { rayon::join(oper_a, oper_b) }
287    }
288
289    /// Executes the given closure in a fork-join scope.
290    ///
291    /// See [`rayon::scope`] for more details.
292    #[inline]
293    pub fn scope<'scope, OP, R>(&self, op: OP) -> R
294    where
295        OP: FnOnce(solar_data_structures::sync::Scope<'_, 'scope>) -> R + Send,
296        R: Send,
297    {
298        solar_data_structures::sync::scope(self.is_parallel(), op)
299    }
300
301    /// Sets up the session globals in the current thread, then executes the given closure.
302    ///
303    /// The globals are stored in this [`Session`] itself, meaning multiple consecutive calls to
304    /// [`enter`](Self::enter) will share the same globals.
305    ///
306    /// Note that this does not set up the rayon thread pool. This is only useful when parsing
307    /// sequentially, like manually using `Parser`.
308    #[inline]
309    #[track_caller]
310    pub fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
311        self.globals.set(f)
312    }
313
314    /// Sets up a thread pool if parallelism is enabled, setting up the session globals on every
315    /// thread.
316    ///
317    /// See [`enter`](Self::enter) for more details.
318    #[inline]
319    #[track_caller]
320    pub fn enter_parallel<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
321        self.enter(|| enter_thread_pool(self, f))
322    }
323}
324
325/// Runs the given closure in a thread pool with the given number of threads.
326#[track_caller]
327fn enter_thread_pool<R: Send>(sess: &Session, f: impl FnOnce() -> R + Send) -> R {
328    // Avoid panicking below if this is a recursive call.
329    if rayon::current_thread_index().is_some() {
330        debug!(
331            "running in the current thread's rayon thread pool; \
332             this could cause panics later on if it was created without setting the session globals!"
333        );
334        return f();
335    }
336
337    let threads = sess.threads();
338    debug_assert!(threads > 0, "number of threads must already be resolved");
339    let mut builder =
340        rayon::ThreadPoolBuilder::new().thread_name(|i| format!("solar-{i}")).num_threads(threads);
341    // We still want to use a rayon thread pool with 1 thread so that `ParallelIterator`s don't
342    // install and run in the default global thread pool.
343    if threads == 1 {
344        builder = builder.use_current_thread();
345    }
346    match builder.build_scoped(
347        // Initialize each new worker thread when created.
348        // Note that this is not called on the current thread, so `SessionGlobals::set` can't
349        // panic.
350        move |thread| sess.enter(|| thread.run()),
351        // Run `f` on the first thread in the thread pool.
352        move |pool| pool.install(f),
353    ) {
354        Ok(r) => r,
355        Err(e) => {
356            let mut err = sess.dcx.fatal(format!("failed to build the rayon thread pool: {e}"));
357            if threads > 1 {
358                if SINGLE_THREADED_TARGET {
359                    err = err.note("the current target might not support multi-threaded execution");
360                }
361                err = err.help("try running with `--threads 1` / `-j1` to disable parallelism");
362            }
363            err.emit();
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use std::path::PathBuf;
372
373    #[test]
374    #[should_panic = "diagnostics context not set"]
375    fn no_dcx() {
376        Session::builder().build();
377    }
378
379    #[test]
380    #[should_panic = "session source map does not match the one in the diagnostics context"]
381    fn sm_mismatch() {
382        let sm1 = Arc::<SourceMap>::default();
383        let sm2 = Arc::<SourceMap>::default();
384        assert!(!Arc::ptr_eq(&sm1, &sm2));
385        Session::builder().source_map(sm1).dcx(DiagCtxt::with_stderr_emitter(Some(sm2))).build();
386    }
387
388    #[test]
389    #[should_panic = "session source map does not match the one in the diagnostics context"]
390    fn sm_mismatch_non_builder() {
391        let sm1 = Arc::<SourceMap>::default();
392        let sm2 = Arc::<SourceMap>::default();
393        assert!(!Arc::ptr_eq(&sm1, &sm2));
394        Session::new(DiagCtxt::with_stderr_emitter(Some(sm2)), sm1);
395    }
396
397    #[test]
398    fn builder() {
399        let _ = Session::builder().with_stderr_emitter().build();
400    }
401
402    #[test]
403    fn empty() {
404        let _ = Session::empty(DiagCtxt::with_stderr_emitter(None));
405        let _ = Session::empty(DiagCtxt::with_stderr_emitter(Some(Default::default())));
406    }
407
408    #[test]
409    fn local() {
410        let sess = Session::builder().with_stderr_emitter().build();
411        assert!(sess.emitted_diagnostics().is_none());
412        assert!(sess.emitted_errors().is_none());
413
414        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
415        sess.dcx.err("test").emit();
416        let err = sess.dcx.emitted_errors().unwrap().unwrap_err();
417        let err = Box::new(err) as Box<dyn std::error::Error>;
418        assert!(err.to_string().contains("error: test"), "{err:?}");
419    }
420
421    #[test]
422    fn enter() {
423        #[track_caller]
424        fn use_globals_no_sm() {
425            SessionGlobals::with(|_globals| {});
426
427            let s = "hello";
428            let sym = crate::Symbol::intern(s);
429            assert_eq!(sym.as_str(), s);
430        }
431
432        #[track_caller]
433        fn use_globals() {
434            use_globals_no_sm();
435
436            let span = crate::Span::new(crate::BytePos(0), crate::BytePos(1));
437            assert_eq!(format!("{span:?}"), "test:1:1: 1:2");
438            assert_eq!(format!("{span:#?}"), "test:1:1: 1:2");
439
440            assert!(rayon::current_thread_index().is_some());
441        }
442
443        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
444        sess.source_map().new_source_file(PathBuf::from("test"), "abcd").unwrap();
445        sess.enter_parallel(|| use_globals());
446        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
447        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
448        sess.enter_parallel(|| {
449            use_globals();
450            sess.enter_parallel(use_globals);
451            use_globals();
452        });
453        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
454        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
455
456        sess.enter(|| {
457            use_globals_no_sm();
458            sess.enter_parallel(|| {
459                use_globals();
460                sess.enter_parallel(|| use_globals());
461                use_globals();
462            });
463            use_globals_no_sm();
464        });
465        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
466        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
467    }
468
469    #[test]
470    fn enter_diags() {
471        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
472        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
473        sess.enter_parallel(|| {
474            sess.dcx.err("test1").emit();
475            assert!(sess.dcx.emitted_errors().unwrap().is_err());
476        });
477        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test1"));
478        sess.enter_parallel(|| {
479            sess.dcx.err("test2").emit();
480            assert!(sess.dcx.emitted_errors().unwrap().is_err());
481        });
482        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test1"));
483        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test2"));
484    }
485
486    #[test]
487    fn set_opts() {
488        let _ = Session::builder()
489            .with_test_emitter()
490            .opts(Opts {
491                evm_version: solar_config::EvmVersion::Berlin,
492                unstable: UnstableOpts { ast_stats: false, ..Default::default() },
493                ..Default::default()
494            })
495            .build();
496    }
497}