solar_interface/
session.rs

1use crate::{
2    diagnostics::{DiagCtxt, EmittedDiagnostics},
3    ColorChoice, SessionGlobals, SourceMap,
4};
5use solar_config::{CompilerOutput, CompilerStage, Opts, UnstableOpts};
6use std::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| 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.threads(), 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    threads: usize,
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 mut builder =
312        rayon::ThreadPoolBuilder::new().thread_name(|i| format!("solar-{i}")).num_threads(threads);
313    // We still want to use a rayon thread pool with 1 thread so that `ParallelIterator`s don't
314    // install and run in the default global thread pool.
315    if threads == 1 {
316        builder = builder.use_current_thread();
317    }
318    builder
319        .build_scoped(
320            // Initialize each new worker thread when created.
321            // Note that this is not called on the current thread, so `set` can't panic.
322            move |thread| session_globals.set(|| thread.run()),
323            // Run `f` on the first thread in the thread pool.
324            move |pool| pool.install(f),
325        )
326        .unwrap()
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    #[should_panic = "diagnostics context not set"]
335    fn no_dcx() {
336        Session::builder().build();
337    }
338
339    #[test]
340    #[should_panic = "session source map does not match the one in the diagnostics context"]
341    fn sm_mismatch() {
342        let sm1 = Arc::<SourceMap>::default();
343        let sm2 = Arc::<SourceMap>::default();
344        assert!(!Arc::ptr_eq(&sm1, &sm2));
345        Session::builder().source_map(sm1).dcx(DiagCtxt::with_stderr_emitter(Some(sm2))).build();
346    }
347
348    #[test]
349    #[should_panic = "session source map does not match the one in the diagnostics context"]
350    fn sm_mismatch_non_builder() {
351        let sm1 = Arc::<SourceMap>::default();
352        let sm2 = Arc::<SourceMap>::default();
353        assert!(!Arc::ptr_eq(&sm1, &sm2));
354        Session::new(DiagCtxt::with_stderr_emitter(Some(sm2)), sm1);
355    }
356
357    #[test]
358    fn builder() {
359        let _ = Session::builder().with_stderr_emitter().build();
360    }
361
362    #[test]
363    fn empty() {
364        let _ = Session::empty(DiagCtxt::with_stderr_emitter(None));
365        let _ = Session::empty(DiagCtxt::with_stderr_emitter(Some(Default::default())));
366    }
367
368    #[test]
369    fn local() {
370        let sess = Session::builder().with_stderr_emitter().build();
371        assert!(sess.emitted_diagnostics().is_none());
372        assert!(sess.emitted_errors().is_none());
373
374        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
375        sess.dcx.err("test").emit();
376        let err = sess.dcx.emitted_errors().unwrap().unwrap_err();
377        let err = Box::new(err) as Box<dyn std::error::Error>;
378        assert!(err.to_string().contains("error: test"), "{err:?}");
379    }
380
381    #[test]
382    fn enter() {
383        #[track_caller]
384        fn use_globals_no_sm() {
385            SessionGlobals::with(|_globals| {});
386
387            let s = "hello";
388            let sym = crate::Symbol::intern(s);
389            assert_eq!(sym.as_str(), s);
390        }
391
392        #[track_caller]
393        fn use_globals() {
394            use_globals_no_sm();
395
396            let span = crate::Span::new(crate::BytePos(0), crate::BytePos(1));
397            let s = format!("{span:?}");
398            assert!(!s.contains("Span("), "{s}");
399            let s = format!("{span:#?}");
400            assert!(!s.contains("Span("), "{s}");
401
402            assert!(rayon::current_thread_index().is_some());
403        }
404
405        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
406        sess.enter_parallel(use_globals);
407        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
408        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
409        sess.enter_parallel(|| {
410            use_globals();
411            sess.enter_parallel(use_globals);
412            use_globals();
413        });
414        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
415        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
416
417        SessionGlobals::new().set(|| {
418            use_globals_no_sm();
419            sess.enter_parallel(|| {
420                use_globals();
421                sess.enter_parallel(use_globals);
422                use_globals();
423            });
424            use_globals_no_sm();
425        });
426        assert!(sess.dcx.emitted_diagnostics().unwrap().is_empty());
427        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
428    }
429
430    #[test]
431    fn enter_diags() {
432        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
433        assert!(sess.dcx.emitted_errors().unwrap().is_ok());
434        sess.enter_parallel(|| {
435            sess.dcx.err("test1").emit();
436            assert!(sess.dcx.emitted_errors().unwrap().is_err());
437        });
438        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test1"));
439        sess.enter_parallel(|| {
440            sess.dcx.err("test2").emit();
441            assert!(sess.dcx.emitted_errors().unwrap().is_err());
442        });
443        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test1"));
444        assert!(sess.dcx.emitted_errors().unwrap().unwrap_err().to_string().contains("test2"));
445    }
446}