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::{
7    path::Path,
8    sync::{Arc, OnceLock},
9};
10
11/// Information about the current compiler session.
12pub struct Session {
13    /// The compiler options.
14    pub opts: Opts,
15
16    /// The diagnostics context.
17    pub dcx: DiagCtxt,
18    /// The globals.
19    globals: Arc<SessionGlobals>,
20    /// The rayon thread pool. This is spawned lazily on first use, rather than always constructing
21    /// one with `SessionBuilder`.
22    thread_pool: OnceLock<rayon::ThreadPool>,
23}
24
25impl Default for Session {
26    fn default() -> Self {
27        Self::new(Opts::default())
28    }
29}
30
31/// [`Session`] builder.
32#[derive(Default)]
33#[must_use = "builders don't do anything unless you call `build`"]
34pub struct SessionBuilder {
35    dcx: Option<DiagCtxt>,
36    globals: Option<SessionGlobals>,
37    opts: Option<Opts>,
38}
39
40impl SessionBuilder {
41    /// Sets the diagnostic context.
42    ///
43    /// If `opts` is set this will default to [`DiagCtxt::from_opts`], otherwise this is required.
44    ///
45    /// See also the `with_*_emitter*` methods.
46    pub fn dcx(mut self, dcx: DiagCtxt) -> Self {
47        self.dcx = Some(dcx);
48        self
49    }
50
51    /// Sets the source map.
52    pub fn source_map(mut self, source_map: Arc<SourceMap>) -> Self {
53        self.get_globals().source_map = source_map;
54        self
55    }
56
57    /// Sets the compiler options.
58    pub fn opts(mut self, opts: Opts) -> Self {
59        self.opts = Some(opts);
60        self
61    }
62
63    /// Sets the diagnostic context to a test emitter.
64    #[inline]
65    pub fn with_test_emitter(mut self) -> Self {
66        let sm = self.get_source_map();
67        self.dcx(DiagCtxt::with_test_emitter(Some(sm)))
68    }
69
70    /// Sets the diagnostic context to a stderr emitter.
71    #[inline]
72    pub fn with_stderr_emitter(self) -> Self {
73        self.with_stderr_emitter_and_color(ColorChoice::Auto)
74    }
75
76    /// Sets the diagnostic context to a stderr emitter and a color choice.
77    #[inline]
78    pub fn with_stderr_emitter_and_color(mut self, color_choice: ColorChoice) -> Self {
79        let sm = self.get_source_map();
80        self.dcx(DiagCtxt::with_stderr_emitter_and_color(Some(sm), color_choice))
81    }
82
83    /// Sets the diagnostic context to a human emitter that emits diagnostics to a local buffer.
84    #[inline]
85    pub fn with_buffer_emitter(mut self, color_choice: ColorChoice) -> Self {
86        let sm = self.get_source_map();
87        self.dcx(DiagCtxt::with_buffer_emitter(Some(sm), color_choice))
88    }
89
90    /// Sets the diagnostic context to a silent emitter.
91    #[inline]
92    pub fn with_silent_emitter(self, fatal_note: Option<String>) -> Self {
93        self.dcx(DiagCtxt::with_silent_emitter(fatal_note))
94    }
95
96    /// Sets the number of threads to use for parallelism to 1.
97    #[inline]
98    pub fn single_threaded(self) -> Self {
99        self.threads(1)
100    }
101
102    /// Sets the number of threads to use for parallelism. Zero specifies the number of logical
103    /// cores.
104    #[inline]
105    pub fn threads(mut self, threads: usize) -> Self {
106        self.opts_mut().threads = threads.into();
107        self
108    }
109
110    /// Gets the source map from the diagnostics context.
111    fn get_source_map(&mut self) -> Arc<SourceMap> {
112        self.get_globals().source_map.clone()
113    }
114
115    fn get_globals(&mut self) -> &mut SessionGlobals {
116        self.globals.get_or_insert_default()
117    }
118
119    /// Returns a mutable reference to the options.
120    fn opts_mut(&mut self) -> &mut Opts {
121        self.opts.get_or_insert_default()
122    }
123
124    /// Consumes the builder to create a new session.
125    ///
126    /// The diagnostics context must be set before calling this method, either by calling
127    /// [`dcx`](Self::dcx) or by using one of the provided helper methods, like
128    /// [`with_stderr_emitter`](Self::with_stderr_emitter).
129    ///
130    /// # Panics
131    ///
132    /// Panics if:
133    /// - the diagnostics context is not set
134    /// - the source map in the diagnostics context does not match the one set in the builder
135    #[track_caller]
136    pub fn build(mut self) -> Session {
137        let opts = self.opts.take();
138        let mut dcx = self.dcx.take().unwrap_or_else(|| {
139            opts.as_ref()
140                .map(DiagCtxt::from_opts)
141                .unwrap_or_else(|| panic!("either diagnostics context or options must be set"))
142        });
143        let sess = Session {
144            globals: Arc::new(match self.globals.take() {
145                Some(globals) => {
146                    // Check that the source map matches the one in the diagnostics context.
147                    if let Some(sm) = dcx.source_map_mut() {
148                        assert!(
149                            Arc::ptr_eq(&globals.source_map, sm),
150                            "session source map does not match the one in the diagnostics context"
151                        );
152                    }
153                    globals
154                }
155                None => {
156                    // Set the source map from the diagnostics context.
157                    let sm = dcx.source_map_mut().cloned().unwrap_or_default();
158                    SessionGlobals::new(sm)
159                }
160            }),
161            dcx,
162            opts: opts.unwrap_or_default(),
163            thread_pool: OnceLock::new(),
164        };
165        sess.reconfigure();
166        debug!(version = %solar_config::version::SEMVER_VERSION, "created new session");
167        sess
168    }
169}
170
171impl Session {
172    /// Creates a new session builder.
173    #[inline]
174    pub fn builder() -> SessionBuilder {
175        SessionBuilder::default()
176    }
177
178    /// Creates a new session from the given options.
179    ///
180    /// See [`builder`](Self::builder) for a more flexible way to create a session.
181    pub fn new(opts: Opts) -> Self {
182        Self::builder().opts(opts).build()
183    }
184
185    /// Infers the language from the input files.
186    pub fn infer_language(&mut self) {
187        if !self.opts.input.is_empty()
188            && self.opts.input.iter().all(|arg| Path::new(arg).extension() == Some("yul".as_ref()))
189        {
190            self.opts.language = solar_config::Language::Yul;
191        }
192    }
193
194    /// Validates the session options.
195    pub fn validate(&self) -> crate::Result<()> {
196        let mut result = Ok(());
197        result = result.and(self.check_unique("emit", &self.opts.emit));
198        result
199    }
200
201    /// Reconfigures inner state to match any new options.
202    ///
203    /// Call this after updating options.
204    pub fn reconfigure(&self) {
205        'bp: {
206            let new_base_path = if self.opts.unstable.ui_testing {
207                // `ui_test` relies on absolute paths.
208                None
209            } else if let Some(base_path) =
210                self.opts.base_path.clone().or_else(|| std::env::current_dir().ok())
211                && let Ok(base_path) = self.source_map().file_loader().canonicalize_path(&base_path)
212            {
213                Some(base_path)
214            } else {
215                break 'bp;
216            };
217            self.source_map().set_base_path(new_base_path);
218        }
219    }
220
221    fn check_unique<T: Eq + std::hash::Hash + std::fmt::Display>(
222        &self,
223        name: &str,
224        list: &[T],
225    ) -> crate::Result<()> {
226        let mut result = Ok(());
227        let mut seen = std::collections::HashSet::new();
228        for item in list {
229            if !seen.insert(item) {
230                let msg = format!("cannot specify `--{name} {item}` twice");
231                result = Err(self.dcx.err(msg).emit());
232            }
233        }
234        result
235    }
236
237    /// Returns the unstable options.
238    #[inline]
239    pub fn unstable(&self) -> &UnstableOpts {
240        &self.opts.unstable
241    }
242
243    /// Returns the emitted diagnostics as a result. Can be empty.
244    ///
245    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
246    /// [`with_buffer_emitter`](SessionBuilder::with_buffer_emitter).
247    ///
248    /// Results `Ok` if there are no errors, `Err` otherwise.
249    ///
250    /// # Examples
251    ///
252    /// Print diagnostics to `stdout` if there are no errors, otherwise propagate with `?`:
253    ///
254    /// ```no_run
255    /// # fn f(dcx: solar_interface::diagnostics::DiagCtxt) -> Result<(), Box<dyn std::error::Error>> {
256    /// println!("{}", dcx.emitted_diagnostics_result().unwrap()?);
257    /// # Ok(())
258    /// # }
259    /// ```
260    #[inline]
261    pub fn emitted_diagnostics_result(
262        &self,
263    ) -> Option<Result<EmittedDiagnostics, EmittedDiagnostics>> {
264        self.dcx.emitted_diagnostics_result()
265    }
266
267    /// Returns the emitted diagnostics. Can be empty.
268    ///
269    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
270    /// [`with_buffer_emitter`](SessionBuilder::with_buffer_emitter).
271    #[inline]
272    pub fn emitted_diagnostics(&self) -> Option<EmittedDiagnostics> {
273        self.dcx.emitted_diagnostics()
274    }
275
276    /// Returns `Err` with the printed diagnostics if any errors have been emitted.
277    ///
278    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
279    /// [`with_buffer_emitter`](SessionBuilder::with_buffer_emitter).
280    #[inline]
281    pub fn emitted_errors(&self) -> Option<Result<(), EmittedDiagnostics>> {
282        self.dcx.emitted_errors()
283    }
284
285    /// Returns a reference to the source map.
286    #[inline]
287    pub fn source_map(&self) -> &SourceMap {
288        &self.globals.source_map
289    }
290
291    /// Clones the source map.
292    #[inline]
293    pub fn clone_source_map(&self) -> Arc<SourceMap> {
294        self.globals.source_map.clone()
295    }
296
297    /// Returns `true` if compilation should stop after the given stage.
298    #[inline]
299    pub fn stop_after(&self, stage: CompilerStage) -> bool {
300        self.opts.stop_after >= Some(stage)
301    }
302
303    /// Returns the number of threads to use for parallelism.
304    #[inline]
305    pub fn threads(&self) -> usize {
306        self.opts.threads().get()
307    }
308
309    /// Returns `true` if parallelism is not enabled.
310    #[inline]
311    pub fn is_sequential(&self) -> bool {
312        self.threads() == 1
313    }
314
315    /// Returns `true` if parallelism is enabled.
316    #[inline]
317    pub fn is_parallel(&self) -> bool {
318        !self.is_sequential()
319    }
320
321    /// Returns `true` if the given output should be emitted.
322    #[inline]
323    pub fn do_emit(&self, output: CompilerOutput) -> bool {
324        self.opts.emit.contains(&output)
325    }
326
327    /// Spawns the given closure on the thread pool or executes it immediately if parallelism is not
328    /// enabled.
329    ///
330    /// NOTE: on a `use_current_thread` thread pool `rayon::spawn` will never execute without
331    /// yielding to rayon, so prefer using this method over `rayon::spawn`.
332    #[inline]
333    pub fn spawn(&self, f: impl FnOnce() + Send + 'static) {
334        if self.is_sequential() {
335            f();
336        } else {
337            rayon::spawn(f);
338        }
339    }
340
341    /// Takes two closures and potentially runs them in parallel. It returns a pair of the results
342    /// from those closures.
343    ///
344    /// NOTE: on a `use_current_thread` thread pool `rayon::join` will never execute without
345    /// yielding to rayon, so prefer using this method over `rayon::join`.
346    #[inline]
347    pub fn join<A, B, RA, RB>(&self, oper_a: A, oper_b: B) -> (RA, RB)
348    where
349        A: FnOnce() -> RA + Send,
350        B: FnOnce() -> RB + Send,
351        RA: Send,
352        RB: Send,
353    {
354        if self.is_sequential() { (oper_a(), oper_b()) } else { rayon::join(oper_a, oper_b) }
355    }
356
357    /// Executes the given closure in a fork-join scope.
358    ///
359    /// See [`rayon::scope`] for more details.
360    #[inline]
361    pub fn scope<'scope, OP, R>(&self, op: OP) -> R
362    where
363        OP: FnOnce(solar_data_structures::sync::Scope<'_, 'scope>) -> R + Send,
364        R: Send,
365    {
366        solar_data_structures::sync::scope(self.is_parallel(), op)
367    }
368
369    /// Sets up the session globals and executes the given closure in the thread pool.
370    ///
371    /// The thread pool and globals are stored in this [`Session`] itself, meaning multiple
372    /// consecutive calls to [`enter`](Self::enter) will share the same globals and resources.
373    #[track_caller]
374    pub fn enter<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
375        if in_rayon() {
376            // Avoid panicking if we were to build a `current_thread` thread pool.
377            if self.is_sequential() {
378                reentrant_log();
379                return self.enter_sequential(f);
380            }
381            // Avoid creating a new thread pool if it's already set up with the same globals.
382            if self.is_set() {
383                // No need to set again.
384                return f();
385            }
386        }
387
388        self.enter_sequential(|| self.thread_pool().install(f))
389    }
390
391    /// Sets up the session globals and executes the given closure in the current thread.
392    ///
393    /// Note that this does not set up the rayon thread pool. This is only useful when parsing
394    /// sequentially, like manually using `Parser`. Otherwise, it might cause panics later on if a
395    /// thread pool is expected to be set up correctly.
396    ///
397    /// See [`enter`](Self::enter) for more details.
398    #[inline]
399    #[track_caller]
400    pub fn enter_sequential<R>(&self, f: impl FnOnce() -> R) -> R {
401        self.globals.set(f)
402    }
403
404    /// Returns `true` if the session globals are already set to this instance's.
405    fn is_set(&self) -> bool {
406        SessionGlobals::try_with(|g| g.is_some_and(|g| g.maybe_eq(&self.globals)))
407    }
408
409    fn thread_pool(&self) -> &rayon::ThreadPool {
410        self.thread_pool.get_or_init(|| {
411            trace!(threads = self.threads(), "building rayon thread pool");
412            self.thread_pool_builder()
413                .spawn_handler(|thread| {
414                    let mut builder = std::thread::Builder::new();
415                    if let Some(name) = thread.name() {
416                        builder = builder.name(name.to_string());
417                    }
418                    if let Some(size) = thread.stack_size() {
419                        builder = builder.stack_size(size);
420                    }
421                    let globals = self.globals.clone();
422                    builder.spawn(move || globals.set(|| thread.run()))?;
423                    Ok(())
424                })
425                .build()
426                .unwrap_or_else(|e| self.handle_thread_pool_build_error(e))
427        })
428    }
429
430    fn thread_pool_builder(&self) -> rayon::ThreadPoolBuilder {
431        let threads = self.threads();
432        debug_assert!(threads > 0, "number of threads must already be resolved");
433        let mut builder = rayon::ThreadPoolBuilder::new()
434            .thread_name(|i| format!("solar-{i}"))
435            .num_threads(threads);
436        // We still want to use a rayon thread pool with 1 thread so that `ParallelIterator`s don't
437        // install and run in the default global thread pool.
438        if threads == 1 {
439            builder = builder.use_current_thread();
440        }
441        builder
442    }
443
444    #[cold]
445    fn handle_thread_pool_build_error(&self, e: rayon::ThreadPoolBuildError) -> ! {
446        let mut err = self.dcx.fatal(format!("failed to build the rayon thread pool: {e}"));
447        if self.is_parallel() {
448            if SINGLE_THREADED_TARGET {
449                err = err.note("the current target might not support multi-threaded execution");
450            }
451            err = err.help("try running with `--threads 1` / `-j1` to disable parallelism");
452        }
453        err.emit()
454    }
455}
456
457fn reentrant_log() {
458    debug!(
459        "running in the current thread's rayon thread pool; \
460         this could cause panics later on if it was created without setting the session globals!"
461    );
462}
463
464#[inline]
465fn in_rayon() -> bool {
466    rayon::current_thread_index().is_some()
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use std::path::PathBuf;
473
474    /// Session to test `enter`.
475    fn enter_tests_session() -> Session {
476        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
477        sess.source_map().new_source_file(PathBuf::from("test"), "abcd").unwrap();
478        sess
479    }
480
481    #[track_caller]
482    fn use_globals_parallel(sess: &Session) {
483        use rayon::prelude::*;
484
485        use_globals();
486        sess.spawn(|| use_globals());
487        sess.join(|| use_globals(), || use_globals());
488        [1, 2, 3].par_iter().for_each(|_| use_globals());
489        use_globals();
490    }
491
492    #[track_caller]
493    fn use_globals() {
494        use_globals_no_sm();
495
496        let span = crate::Span::new(crate::BytePos(0), crate::BytePos(1));
497        assert_eq!(format!("{span:?}"), "test:1:1: 1:2");
498        assert_eq!(format!("{span:#?}"), "test:1:1: 1:2");
499    }
500
501    #[track_caller]
502    fn use_globals_no_sm() {
503        SessionGlobals::with(|_globals| {});
504
505        let s = "hello";
506        let sym = crate::Symbol::intern(s);
507        assert_eq!(sym.as_str(), s);
508    }
509
510    #[track_caller]
511    fn cant_use_globals() {
512        std::panic::catch_unwind(|| use_globals()).unwrap_err();
513    }
514
515    #[test]
516    fn builder() {
517        let _ = Session::builder().with_stderr_emitter().build();
518    }
519
520    #[test]
521    fn not_builder() {
522        let _ = Session::new(Opts::default());
523        let _ = Session::default();
524    }
525
526    #[test]
527    #[should_panic = "session source map does not match the one in the diagnostics context"]
528    fn sm_mismatch() {
529        let sm1 = Arc::<SourceMap>::default();
530        let sm2 = Arc::<SourceMap>::default();
531        assert!(!Arc::ptr_eq(&sm1, &sm2));
532        Session::builder().source_map(sm1).dcx(DiagCtxt::with_stderr_emitter(Some(sm2))).build();
533    }
534
535    #[test]
536    #[should_panic = "either diagnostics context or options must be set"]
537    fn no_dcx() {
538        Session::builder().build();
539    }
540
541    #[test]
542    fn dcx() {
543        let _ = Session::builder().dcx(DiagCtxt::with_stderr_emitter(None)).build();
544        let _ =
545            Session::builder().dcx(DiagCtxt::with_stderr_emitter(Some(Default::default()))).build();
546    }
547
548    #[test]
549    fn local() {
550        let sess = Session::builder().with_stderr_emitter().build();
551        assert!(sess.emitted_diagnostics().is_none());
552        assert!(sess.emitted_errors().is_none());
553
554        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
555        sess.dcx.err("test").emit();
556        let err = sess.dcx.emitted_errors().unwrap().unwrap_err();
557        let err = Box::new(err) as Box<dyn std::error::Error>;
558        assert!(err.to_string().contains("error: test"), "{err:?}");
559    }
560
561    #[test]
562    fn enter() {
563        crate::enter(|| {
564            use_globals_no_sm();
565            cant_use_globals();
566        });
567
568        let sess = enter_tests_session();
569        sess.enter(|| use_globals());
570        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
571        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
572        sess.enter(|| {
573            use_globals();
574            sess.enter(|| use_globals());
575            use_globals();
576        });
577        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
578        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
579
580        sess.enter_sequential(|| {
581            use_globals();
582            sess.enter(|| {
583                use_globals_parallel(&sess);
584                sess.enter(|| use_globals_parallel(&sess));
585                use_globals_parallel(&sess);
586            });
587            use_globals();
588        });
589        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
590        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
591    }
592
593    #[test]
594    fn enter_diags() {
595        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
596        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
597        sess.enter(|| {
598            sess.dcx.err("test1").emit();
599            assert!(sess.dcx.emitted_errors().unwrap().is_err());
600        });
601        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test1"));
602        sess.enter(|| {
603            sess.dcx.err("test2").emit();
604            assert!(sess.dcx.emitted_errors().unwrap().is_err());
605        });
606        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test1"));
607        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test2"));
608    }
609
610    #[test]
611    fn enter_thread_pool() {
612        let sess = enter_tests_session();
613
614        assert!(!in_rayon());
615        sess.enter(|| {
616            assert!(in_rayon());
617            sess.enter(|| {
618                assert!(in_rayon());
619            });
620            assert!(in_rayon());
621        });
622        sess.enter_sequential(|| {
623            assert!(!in_rayon());
624            sess.enter(|| {
625                assert!(in_rayon());
626            });
627            assert!(!in_rayon());
628            sess.enter_sequential(|| {
629                assert!(!in_rayon());
630            });
631            assert!(!in_rayon());
632        });
633        assert!(!in_rayon());
634
635        let pool = rayon::ThreadPoolBuilder::new().build().unwrap();
636        pool.install(|| {
637            assert!(in_rayon());
638            cant_use_globals();
639            sess.enter(|| use_globals_parallel(&sess));
640            assert!(in_rayon());
641            cant_use_globals();
642        });
643        assert!(!in_rayon());
644    }
645
646    #[test]
647    fn enter_different_nested_sessions() {
648        let sess1 = enter_tests_session();
649        let sess2 = enter_tests_session();
650        assert!(!sess1.globals.maybe_eq(&sess2.globals));
651        sess1.enter(|| {
652            SessionGlobals::with(|g| assert!(g.maybe_eq(&sess1.globals)));
653            use_globals();
654            sess2.enter(|| {
655                SessionGlobals::with(|g| assert!(g.maybe_eq(&sess2.globals)));
656                use_globals();
657            });
658            use_globals();
659        });
660    }
661
662    #[test]
663    fn set_opts() {
664        let _ = Session::builder()
665            .with_test_emitter()
666            .opts(Opts {
667                evm_version: solar_config::EvmVersion::Berlin,
668                unstable: UnstableOpts { ast_stats: false, ..Default::default() },
669                ..Default::default()
670            })
671            .build();
672    }
673}