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        let sess = 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        if let Some(base_path) =
147            sess.opts.base_path.clone().or_else(|| std::env::current_dir().ok())
148            && let Ok(base_path) = sess.source_map().file_loader().canonicalize_path(&base_path)
149        {
150            sess.source_map().set_base_path(base_path);
151        }
152
153        sess
154    }
155}
156
157impl Session {
158    /// Creates a new session with the given diagnostics context and source map.
159    pub fn new(dcx: DiagCtxt, source_map: Arc<SourceMap>) -> Self {
160        Self::builder().dcx(dcx).source_map(source_map).build()
161    }
162
163    /// Creates a new session with the given diagnostics context and an empty source map.
164    pub fn empty(dcx: DiagCtxt) -> Self {
165        Self::builder().dcx(dcx).build()
166    }
167
168    /// Creates a new session builder.
169    #[inline]
170    pub fn builder() -> SessionBuilder {
171        SessionBuilder::default()
172    }
173
174    /// Infers the language from the input files.
175    pub fn infer_language(&mut self) {
176        if !self.opts.input.is_empty()
177            && self.opts.input.iter().all(|arg| Path::new(arg).extension() == Some("yul".as_ref()))
178        {
179            self.opts.language = solar_config::Language::Yul;
180        }
181    }
182
183    /// Validates the session options.
184    pub fn validate(&self) -> crate::Result<()> {
185        let mut result = Ok(());
186        result = result.and(self.check_unique("emit", &self.opts.emit));
187        result
188    }
189
190    fn check_unique<T: Eq + std::hash::Hash + std::fmt::Display>(
191        &self,
192        name: &str,
193        list: &[T],
194    ) -> crate::Result<()> {
195        let mut result = Ok(());
196        let mut seen = std::collections::HashSet::new();
197        for item in list {
198            if !seen.insert(item) {
199                let msg = format!("cannot specify `--{name} {item}` twice");
200                result = Err(self.dcx.err(msg).emit());
201            }
202        }
203        result
204    }
205
206    /// Returns the unstable options.
207    #[inline]
208    pub fn unstable(&self) -> &UnstableOpts {
209        &self.opts.unstable
210    }
211
212    /// Returns the emitted diagnostics. Can be empty.
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_diagnostics(&self) -> Option<EmittedDiagnostics> {
218        self.dcx.emitted_diagnostics()
219    }
220
221    /// Returns `Err` with the printed diagnostics if any errors have been emitted.
222    ///
223    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
224    /// [`with_buffer_emitter`](SessionBuilder::with_buffer_emitter).
225    #[inline]
226    pub fn emitted_errors(&self) -> Option<Result<(), EmittedDiagnostics>> {
227        self.dcx.emitted_errors()
228    }
229
230    /// Returns a reference to the source map.
231    #[inline]
232    pub fn source_map(&self) -> &SourceMap {
233        &self.globals.source_map
234    }
235
236    /// Clones the source map.
237    #[inline]
238    pub fn clone_source_map(&self) -> Arc<SourceMap> {
239        self.globals.source_map.clone()
240    }
241
242    /// Returns `true` if compilation should stop after the given stage.
243    #[inline]
244    pub fn stop_after(&self, stage: CompilerStage) -> bool {
245        self.opts.stop_after >= Some(stage)
246    }
247
248    /// Returns the number of threads to use for parallelism.
249    #[inline]
250    pub fn threads(&self) -> usize {
251        self.opts.threads().get()
252    }
253
254    /// Returns `true` if parallelism is not enabled.
255    #[inline]
256    pub fn is_sequential(&self) -> bool {
257        self.threads() == 1
258    }
259
260    /// Returns `true` if parallelism is enabled.
261    #[inline]
262    pub fn is_parallel(&self) -> bool {
263        !self.is_sequential()
264    }
265
266    /// Returns `true` if the given output should be emitted.
267    #[inline]
268    pub fn do_emit(&self, output: CompilerOutput) -> bool {
269        self.opts.emit.contains(&output)
270    }
271
272    /// Spawns the given closure on the thread pool or executes it immediately if parallelism is not
273    /// enabled.
274    ///
275    /// NOTE: on a `use_current_thread` thread pool `rayon::spawn` will never execute without
276    /// yielding to rayon, so prefer using this method over `rayon::spawn`.
277    #[inline]
278    pub fn spawn(&self, f: impl FnOnce() + Send + 'static) {
279        if self.is_sequential() {
280            f();
281        } else {
282            rayon::spawn(f);
283        }
284    }
285
286    /// Takes two closures and potentially runs them in parallel. It returns a pair of the results
287    /// from those closures.
288    ///
289    /// NOTE: on a `use_current_thread` thread pool `rayon::join` will never execute without
290    /// yielding to rayon, so prefer using this method over `rayon::join`.
291    #[inline]
292    pub fn join<A, B, RA, RB>(&self, oper_a: A, oper_b: B) -> (RA, RB)
293    where
294        A: FnOnce() -> RA + Send,
295        B: FnOnce() -> RB + Send,
296        RA: Send,
297        RB: Send,
298    {
299        if self.is_sequential() { (oper_a(), oper_b()) } else { rayon::join(oper_a, oper_b) }
300    }
301
302    /// Executes the given closure in a fork-join scope.
303    ///
304    /// See [`rayon::scope`] for more details.
305    #[inline]
306    pub fn scope<'scope, OP, R>(&self, op: OP) -> R
307    where
308        OP: FnOnce(solar_data_structures::sync::Scope<'_, 'scope>) -> R + Send,
309        R: Send,
310    {
311        solar_data_structures::sync::scope(self.is_parallel(), op)
312    }
313
314    /// Sets up a thread pool and the session globals in the current thread, then executes the given
315    /// closure.
316    ///
317    /// The globals are stored in this [`Session`] itself, meaning multiple consecutive calls to
318    /// [`enter`](Self::enter) will share the same globals.
319    #[track_caller]
320    pub fn enter<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
321        if in_rayon() {
322            // Avoid panicking if we were to build a `current_thread` thread pool.
323            if self.is_sequential() {
324                reentrant_log();
325                return self.enter_sequential(f);
326            }
327            // Avoid creating a new thread pool if it's already set up with the same globals.
328            if self.is_set() {
329                // No need to set again.
330                return f();
331            }
332        }
333
334        self.enter_sequential(|| {
335            match thread_pool_builder(self).build_scoped(
336                // Initialize each new worker thread when created.
337                // Note that this is not called on the current thread, so `SessionGlobals::set`
338                // can't panic.
339                thread_wrapper(self),
340                // Run `f` on the first thread in the thread pool.
341                move |pool| pool.install(f),
342            ) {
343                Ok(r) => r,
344                Err(e) => handle_thread_pool_build_error(self, e),
345            }
346        })
347    }
348
349    /// Sets up the session globals in the current thread, then executes the given closure.
350    ///
351    /// Note that this does not set up the rayon thread pool. This is only useful when parsing
352    /// sequentially, like manually using `Parser`. Otherwise, it might cause panics later on if a
353    /// thread pool is expected to be set up correctly.
354    ///
355    /// See [`enter`](Self::enter) for more details.
356    #[inline]
357    #[track_caller]
358    pub fn enter_sequential<R>(&self, f: impl FnOnce() -> R) -> R {
359        self.globals.set(f)
360    }
361
362    /// Returns `true` if the session globals are already set to this instance's.
363    fn is_set(&self) -> bool {
364        SessionGlobals::try_with(|g| g.is_some_and(|g| g.maybe_eq(&self.globals)))
365    }
366}
367
368fn reentrant_log() {
369    debug!(
370        "running in the current thread's rayon thread pool; \
371         this could cause panics later on if it was created without setting the session globals!"
372    );
373}
374
375fn thread_pool_builder(sess: &Session) -> rayon::ThreadPoolBuilder {
376    let threads = sess.threads();
377    debug_assert!(threads > 0, "number of threads must already be resolved");
378    let mut builder =
379        rayon::ThreadPoolBuilder::new().thread_name(|i| format!("solar-{i}")).num_threads(threads);
380    // We still want to use a rayon thread pool with 1 thread so that `ParallelIterator`s don't
381    // install and run in the default global thread pool.
382    if threads == 1 {
383        builder = builder.use_current_thread();
384    }
385    builder
386}
387
388fn thread_wrapper(sess: &Session) -> impl Fn(rayon::ThreadBuilder) {
389    move |thread| sess.enter_sequential(|| thread.run())
390}
391
392#[cold]
393fn handle_thread_pool_build_error(sess: &Session, e: rayon::ThreadPoolBuildError) -> ! {
394    let mut err = sess.dcx.fatal(format!("failed to build the rayon thread pool: {e}"));
395    if sess.is_parallel() {
396        if SINGLE_THREADED_TARGET {
397            err = err.note("the current target might not support multi-threaded execution");
398        }
399        err = err.help("try running with `--threads 1` / `-j1` to disable parallelism");
400    }
401    err.emit()
402}
403
404#[inline]
405fn in_rayon() -> bool {
406    rayon::current_thread_index().is_some()
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use std::path::PathBuf;
413
414    /// Session to test `enter`.
415    fn enter_tests_session() -> Session {
416        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
417        sess.source_map().new_source_file(PathBuf::from("test"), "abcd").unwrap();
418        sess
419    }
420
421    #[track_caller]
422    fn use_globals_parallel(sess: &Session) {
423        use rayon::prelude::*;
424
425        use_globals();
426        sess.spawn(|| use_globals());
427        sess.join(|| use_globals(), || use_globals());
428        [1, 2, 3].par_iter().for_each(|_| use_globals());
429        use_globals();
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
441    #[track_caller]
442    fn use_globals_no_sm() {
443        SessionGlobals::with(|_globals| {});
444
445        let s = "hello";
446        let sym = crate::Symbol::intern(s);
447        assert_eq!(sym.as_str(), s);
448    }
449
450    #[track_caller]
451    fn cant_use_globals() {
452        std::panic::catch_unwind(|| use_globals()).unwrap_err();
453    }
454
455    #[test]
456    #[should_panic = "diagnostics context not set"]
457    fn no_dcx() {
458        Session::builder().build();
459    }
460
461    #[test]
462    #[should_panic = "session source map does not match the one in the diagnostics context"]
463    fn sm_mismatch() {
464        let sm1 = Arc::<SourceMap>::default();
465        let sm2 = Arc::<SourceMap>::default();
466        assert!(!Arc::ptr_eq(&sm1, &sm2));
467        Session::builder().source_map(sm1).dcx(DiagCtxt::with_stderr_emitter(Some(sm2))).build();
468    }
469
470    #[test]
471    #[should_panic = "session source map does not match the one in the diagnostics context"]
472    fn sm_mismatch_non_builder() {
473        let sm1 = Arc::<SourceMap>::default();
474        let sm2 = Arc::<SourceMap>::default();
475        assert!(!Arc::ptr_eq(&sm1, &sm2));
476        Session::new(DiagCtxt::with_stderr_emitter(Some(sm2)), sm1);
477    }
478
479    #[test]
480    fn builder() {
481        let _ = Session::builder().with_stderr_emitter().build();
482    }
483
484    #[test]
485    fn empty() {
486        let _ = Session::empty(DiagCtxt::with_stderr_emitter(None));
487        let _ = Session::empty(DiagCtxt::with_stderr_emitter(Some(Default::default())));
488    }
489
490    #[test]
491    fn local() {
492        let sess = Session::builder().with_stderr_emitter().build();
493        assert!(sess.emitted_diagnostics().is_none());
494        assert!(sess.emitted_errors().is_none());
495
496        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
497        sess.dcx.err("test").emit();
498        let err = sess.dcx.emitted_errors().unwrap().unwrap_err();
499        let err = Box::new(err) as Box<dyn std::error::Error>;
500        assert!(err.to_string().contains("error: test"), "{err:?}");
501    }
502
503    #[test]
504    fn enter() {
505        crate::enter(|| {
506            use_globals_no_sm();
507            cant_use_globals();
508        });
509
510        let sess = enter_tests_session();
511        sess.enter(|| use_globals());
512        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
513        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
514        sess.enter(|| {
515            use_globals();
516            sess.enter(|| use_globals());
517            use_globals();
518        });
519        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
520        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
521
522        sess.enter_sequential(|| {
523            use_globals();
524            sess.enter(|| {
525                use_globals_parallel(&sess);
526                sess.enter(|| use_globals_parallel(&sess));
527                use_globals_parallel(&sess);
528            });
529            use_globals();
530        });
531        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
532        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
533    }
534
535    #[test]
536    fn enter_diags() {
537        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
538        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
539        sess.enter(|| {
540            sess.dcx.err("test1").emit();
541            assert!(sess.dcx.emitted_errors().unwrap().is_err());
542        });
543        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test1"));
544        sess.enter(|| {
545            sess.dcx.err("test2").emit();
546            assert!(sess.dcx.emitted_errors().unwrap().is_err());
547        });
548        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test1"));
549        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test2"));
550    }
551
552    #[test]
553    fn enter_thread_pool() {
554        let sess = enter_tests_session();
555
556        assert!(!in_rayon());
557        sess.enter(|| {
558            assert!(in_rayon());
559            sess.enter(|| {
560                assert!(in_rayon());
561            });
562            assert!(in_rayon());
563        });
564        sess.enter_sequential(|| {
565            assert!(!in_rayon());
566            sess.enter(|| {
567                assert!(in_rayon());
568            });
569            assert!(!in_rayon());
570            sess.enter_sequential(|| {
571                assert!(!in_rayon());
572            });
573            assert!(!in_rayon());
574        });
575        assert!(!in_rayon());
576
577        let pool = rayon::ThreadPoolBuilder::new().build().unwrap();
578        pool.install(|| {
579            assert!(in_rayon());
580            cant_use_globals();
581            sess.enter(|| use_globals_parallel(&sess));
582            assert!(in_rayon());
583            cant_use_globals();
584        });
585        assert!(!in_rayon());
586    }
587
588    #[test]
589    fn enter_different_nested_sessions() {
590        let sess1 = enter_tests_session();
591        let sess2 = enter_tests_session();
592        assert!(!sess1.globals.maybe_eq(&sess2.globals));
593        sess1.enter(|| {
594            SessionGlobals::with(|g| assert!(g.maybe_eq(&sess1.globals)));
595            use_globals();
596            sess2.enter(|| {
597                SessionGlobals::with(|g| assert!(g.maybe_eq(&sess2.globals)));
598                use_globals();
599            });
600            use_globals();
601        });
602    }
603
604    #[test]
605    fn set_opts() {
606        let _ = Session::builder()
607            .with_test_emitter()
608            .opts(Opts {
609                evm_version: solar_config::EvmVersion::Berlin,
610                unstable: UnstableOpts { ast_stats: false, ..Default::default() },
611                ..Default::default()
612            })
613            .build();
614    }
615}