Skip to main content

bhc_session/
lib.rs

1//! Compiler session state, options, and configuration for BHC.
2//!
3//! This crate provides the central session management for the BHC compiler,
4//! including compilation options, diagnostic configuration, and global state
5//! that persists throughout a compilation unit.
6//!
7//! # Overview
8//!
9//! The [`Session`] type is the primary entry point, holding all state needed
10//! for a single compilation. It includes:
11//!
12//! - Compiler options and flags
13//! - Target specification
14//! - Diagnostic handler
15//! - Search paths for modules and libraries
16//!
17//! # Profiles
18//!
19//! BHC supports multiple compilation profiles as specified in H26-SPEC:
20//!
21//! - **Default**: Standard lazy Haskell semantics
22//! - **Server**: Optimized for concurrent server workloads
23//! - **Numeric**: Strict-by-default with fusion guarantees
24//! - **Edge**: Minimal runtime for embedded/WASM targets
25
26#![warn(missing_docs)]
27
28use camino::{Utf8Path, Utf8PathBuf};
29use parking_lot::RwLock;
30use rustc_hash::FxHashSet;
31use serde::{Deserialize, Serialize};
32use std::sync::Arc;
33
34/// The compilation profile determines evaluation semantics and optimization strategy.
35///
36/// See H26-SPEC Section 4 for detailed profile specifications.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub enum Profile {
39    /// Standard lazy Haskell evaluation with GC-managed memory.
40    #[default]
41    Default,
42    /// Optimized for server workloads: concurrency, bounded latency, observability.
43    Server,
44    /// Numeric computing: strict-by-default, unboxed, fusion guaranteed.
45    Numeric,
46    /// Minimal runtime footprint for embedded and WASM targets.
47    Edge,
48    /// Realtime profile: bounded GC pauses (<1ms), arena per-frame.
49    Realtime,
50    /// Bare-metal microcontrollers: no GC, static allocation only.
51    /// Programs with escaping allocations are rejected at compile time.
52    Embedded,
53}
54
55impl Profile {
56    /// Returns true if this profile uses strict evaluation by default.
57    #[must_use]
58    pub const fn is_strict_by_default(self) -> bool {
59        matches!(self, Self::Numeric | Self::Edge | Self::Embedded)
60    }
61
62    /// Returns true if fusion is guaranteed for this profile.
63    #[must_use]
64    pub const fn has_fusion_guarantees(self) -> bool {
65        matches!(self, Self::Numeric)
66    }
67
68    /// Returns true if this profile requires escape analysis.
69    ///
70    /// For Embedded profile, programs with escaping allocations are rejected
71    /// at compile time since there is no GC to manage memory.
72    #[must_use]
73    pub const fn requires_escape_analysis(self) -> bool {
74        matches!(self, Self::Embedded)
75    }
76
77    /// Returns true if this profile has no garbage collector.
78    #[must_use]
79    pub const fn is_gc_free(self) -> bool {
80        matches!(self, Self::Embedded)
81    }
82}
83
84/// Optimization level for code generation.
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
86pub enum OptLevel {
87    /// No optimizations (fastest compilation).
88    None,
89    /// Basic optimizations.
90    #[default]
91    Less,
92    /// Standard optimizations (default).
93    Default,
94    /// Aggressive optimizations.
95    Aggressive,
96    /// Size-optimized output.
97    Size,
98    /// Aggressively size-optimized output.
99    SizeMin,
100}
101
102/// Debug information level.
103#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
104pub enum DebugInfo {
105    /// No debug information.
106    #[default]
107    None,
108    /// Line tables only.
109    LineTablesOnly,
110    /// Full debug information.
111    Full,
112}
113
114/// Output type for compilation.
115#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
116pub enum OutputType {
117    /// Object file (.o).
118    #[default]
119    Object,
120    /// Static library (.a).
121    StaticLib,
122    /// Dynamic library (.so/.dylib/.dll).
123    DynamicLib,
124    /// Executable binary.
125    Executable,
126    /// LLVM IR (.ll).
127    LlvmIr,
128    /// LLVM bitcode (.bc).
129    LlvmBitcode,
130    /// Assembly (.s).
131    Assembly,
132    /// WebAssembly module (.wasm).
133    Wasm,
134}
135
136/// Compiler options that can be set via CLI or configuration.
137#[derive(Clone, Debug, Serialize, Deserialize)]
138pub struct Options {
139    /// The compilation profile to use.
140    pub profile: Profile,
141    /// Optimization level.
142    pub opt_level: OptLevel,
143    /// Debug information level.
144    pub debug_info: DebugInfo,
145    /// Output type.
146    pub output_type: OutputType,
147    /// Target triple (e.g., "x86_64-unknown-linux-gnu").
148    pub target_triple: Option<String>,
149    /// Output path for compiled artifacts.
150    pub output_path: Option<Utf8PathBuf>,
151    /// Search paths for module imports.
152    pub import_paths: Vec<Utf8PathBuf>,
153    /// Search paths for libraries.
154    pub library_paths: Vec<Utf8PathBuf>,
155    /// Libraries to link.
156    pub libraries: Vec<String>,
157    /// Enable all warnings.
158    pub warn_all: bool,
159    /// Treat warnings as errors.
160    pub deny_warnings: bool,
161    /// Generate kernel reports (Numeric profile).
162    pub emit_kernel_report: bool,
163    /// Dump intermediate representations.
164    pub dump_ir: IrDumpOptions,
165    /// Path to the BHC standard library.
166    /// Used for implicit Prelude loading and module resolution.
167    pub stdlib_path: Option<Utf8PathBuf>,
168    /// Hackage package dependencies as "name:version" pairs.
169    /// Example: `["filepath:1.4.100.0", "directory:1.3.8.0"]`
170    pub hackage_packages: Vec<String>,
171}
172
173impl Default for Options {
174    fn default() -> Self {
175        Self {
176            profile: Profile::Default,
177            opt_level: OptLevel::Default,
178            debug_info: DebugInfo::None,
179            output_type: OutputType::Object,
180            target_triple: None,
181            output_path: None,
182            import_paths: Vec::new(),
183            library_paths: Vec::new(),
184            libraries: Vec::new(),
185            warn_all: false,
186            deny_warnings: false,
187            emit_kernel_report: false,
188            dump_ir: IrDumpOptions::default(),
189            stdlib_path: None,
190            hackage_packages: Vec::new(),
191        }
192    }
193}
194
195/// Options for dumping intermediate representations.
196#[derive(Clone, Debug, Default, Serialize, Deserialize)]
197pub struct IrDumpOptions {
198    /// Dump AST after parsing.
199    pub dump_ast: bool,
200    /// Dump Core IR.
201    pub dump_core: bool,
202    /// Dump Core IR after specific passes.
203    pub dump_core_passes: Vec<String>,
204    /// Dump Tensor IR.
205    pub dump_tensor_ir: bool,
206    /// Dump Loop IR.
207    pub dump_loop_ir: bool,
208    /// Dump LLVM IR.
209    pub dump_llvm: bool,
210}
211
212/// Search path configuration for finding modules and libraries.
213#[derive(Clone, Debug, Default)]
214pub struct SearchPaths {
215    /// Paths to search for source modules.
216    pub module_paths: Vec<Utf8PathBuf>,
217    /// Paths to search for interface files (.bhi).
218    pub interface_paths: Vec<Utf8PathBuf>,
219    /// Paths to search for libraries.
220    pub library_paths: Vec<Utf8PathBuf>,
221}
222
223impl SearchPaths {
224    /// Create a new empty search path configuration.
225    #[must_use]
226    pub fn new() -> Self {
227        Self::default()
228    }
229
230    /// Add a module search path.
231    pub fn add_module_path(&mut self, path: impl Into<Utf8PathBuf>) {
232        self.module_paths.push(path.into());
233    }
234
235    /// Add an interface search path.
236    pub fn add_interface_path(&mut self, path: impl Into<Utf8PathBuf>) {
237        self.interface_paths.push(path.into());
238    }
239
240    /// Add a library search path.
241    pub fn add_library_path(&mut self, path: impl Into<Utf8PathBuf>) {
242        self.library_paths.push(path.into());
243    }
244}
245
246/// Errors that can occur during session operations.
247#[derive(Debug, thiserror::Error)]
248pub enum SessionError {
249    /// Configuration file not found.
250    #[error("configuration file not found: {0}")]
251    ConfigNotFound(Utf8PathBuf),
252    /// Invalid configuration file.
253    #[error("invalid configuration: {0}")]
254    InvalidConfig(String),
255    /// Target not supported.
256    #[error("unsupported target: {0}")]
257    UnsupportedTarget(String),
258    /// IO error.
259    #[error("IO error: {0}")]
260    Io(#[from] std::io::Error),
261}
262
263/// The compiler session holds all state for a compilation unit.
264///
265/// This is the primary type for managing compilation state and should be
266/// created at the start of compilation and passed through all phases.
267pub struct Session {
268    /// Compiler options.
269    pub options: Options,
270    /// Search paths for modules and libraries.
271    pub search_paths: SearchPaths,
272    /// Set of already-loaded module names (for cycle detection).
273    loaded_modules: RwLock<FxHashSet<String>>,
274    /// Working directory for the session.
275    working_dir: Utf8PathBuf,
276}
277
278impl Session {
279    /// Create a new session with the given options.
280    ///
281    /// # Errors
282    ///
283    /// Returns an error if the current working directory cannot be determined.
284    pub fn new(options: Options) -> Result<Self, SessionError> {
285        let working_dir = std::env::current_dir()
286            .map_err(SessionError::Io)?
287            .try_into()
288            .map_err(|e| SessionError::InvalidConfig(format!("invalid working dir: {e}")))?;
289
290        Ok(Self {
291            options,
292            search_paths: SearchPaths::default(),
293            loaded_modules: RwLock::new(FxHashSet::default()),
294            working_dir,
295        })
296    }
297
298    /// Create a new session with default options.
299    ///
300    /// # Errors
301    ///
302    /// Returns an error if the current working directory cannot be determined.
303    pub fn with_defaults() -> Result<Self, SessionError> {
304        Self::new(Options::default())
305    }
306
307    /// Get the working directory for this session.
308    #[must_use]
309    pub fn working_dir(&self) -> &Utf8Path {
310        &self.working_dir
311    }
312
313    /// Get the compilation profile.
314    #[must_use]
315    pub fn profile(&self) -> Profile {
316        self.options.profile
317    }
318
319    /// Check if a module has been loaded (for cycle detection).
320    #[must_use]
321    pub fn is_module_loaded(&self, name: &str) -> bool {
322        self.loaded_modules.read().contains(name)
323    }
324
325    /// Mark a module as loaded.
326    pub fn mark_module_loaded(&self, name: String) {
327        self.loaded_modules.write().insert(name);
328    }
329
330    /// Get the output path, computing a default if not specified.
331    #[must_use]
332    pub fn output_path(&self, input_name: &str) -> Utf8PathBuf {
333        if let Some(ref path) = self.options.output_path {
334            path.clone()
335        } else {
336            let stem = Utf8Path::new(input_name).file_stem().unwrap_or(input_name);
337            let ext = match self.options.output_type {
338                OutputType::Object => "o",
339                OutputType::StaticLib => "a",
340                OutputType::DynamicLib => {
341                    if cfg!(target_os = "macos") {
342                        "dylib"
343                    } else if cfg!(target_os = "windows") {
344                        "dll"
345                    } else {
346                        "so"
347                    }
348                }
349                OutputType::Executable => "",
350                OutputType::LlvmIr => "ll",
351                OutputType::LlvmBitcode => "bc",
352                OutputType::Assembly => "s",
353                OutputType::Wasm => "wasm",
354            };
355            if ext.is_empty() {
356                Utf8PathBuf::from(stem)
357            } else {
358                Utf8PathBuf::from(format!("{stem}.{ext}"))
359            }
360        }
361    }
362}
363
364/// A shared, thread-safe reference to a session.
365pub type SessionRef = Arc<Session>;
366
367/// Create a shared session reference.
368#[must_use]
369pub fn create_session(options: Options) -> Result<SessionRef, SessionError> {
370    Ok(Arc::new(Session::new(options)?))
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_profile_properties() {
379        assert!(!Profile::Default.is_strict_by_default());
380        assert!(!Profile::Server.is_strict_by_default());
381        assert!(Profile::Numeric.is_strict_by_default());
382        assert!(Profile::Edge.is_strict_by_default());
383
384        assert!(Profile::Numeric.has_fusion_guarantees());
385        assert!(!Profile::Default.has_fusion_guarantees());
386    }
387
388    #[test]
389    fn test_session_module_tracking() {
390        let session = Session::with_defaults().unwrap();
391        assert!(!session.is_module_loaded("Data.List"));
392        session.mark_module_loaded("Data.List".to_string());
393        assert!(session.is_module_loaded("Data.List"));
394    }
395}