solar_interface/
session.rs

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