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 /// Explicitly enable the tensor fusion pipeline regardless of profile.
164 /// The Numeric profile always fuses; this requests fusion otherwise.
165 pub tensor_fusion: bool,
166 /// Dump intermediate representations.
167 pub dump_ir: IrDumpOptions,
168 /// Path to the BHC standard library.
169 /// Used for implicit Prelude loading and module resolution.
170 pub stdlib_path: Option<Utf8PathBuf>,
171 /// Hackage package dependencies as "name:version" pairs.
172 /// Example: `["filepath:1.4.100.0", "directory:1.3.8.0"]`
173 pub hackage_packages: Vec<String>,
174 /// Compile-only mode: produce .o files without linking.
175 pub compile_only: bool,
176 /// Output directory for object files (used with compile_only).
177 pub output_object_dir: Option<Utf8PathBuf>,
178 /// Output directory for interface files (used with compile_only).
179 pub output_interface_dir: Option<Utf8PathBuf>,
180 /// Package database paths for dependency lookup.
181 pub package_dbs: Vec<Utf8PathBuf>,
182 /// Exposed dependency package IDs.
183 pub package_ids: Vec<String>,
184 /// Enabled language extensions (e.g., "OverloadedStrings").
185 pub extensions: Vec<String>,
186 /// CPP preprocessor defines (e.g., "FOO", "BAR=1").
187 pub cpp_defines: Vec<String>,
188}
189
190impl Default for Options {
191 fn default() -> Self {
192 Self {
193 profile: Profile::Default,
194 opt_level: OptLevel::Default,
195 debug_info: DebugInfo::None,
196 output_type: OutputType::Object,
197 target_triple: None,
198 output_path: None,
199 import_paths: Vec::new(),
200 library_paths: Vec::new(),
201 libraries: Vec::new(),
202 warn_all: false,
203 deny_warnings: false,
204 emit_kernel_report: false,
205 tensor_fusion: false,
206 dump_ir: IrDumpOptions::default(),
207 stdlib_path: None,
208 hackage_packages: Vec::new(),
209 compile_only: false,
210 output_object_dir: None,
211 output_interface_dir: None,
212 package_dbs: Vec::new(),
213 package_ids: Vec::new(),
214 extensions: Vec::new(),
215 cpp_defines: Vec::new(),
216 }
217 }
218}
219
220/// Options for dumping intermediate representations.
221#[derive(Clone, Debug, Default, Serialize, Deserialize)]
222pub struct IrDumpOptions {
223 /// Dump AST after parsing.
224 pub dump_ast: bool,
225 /// Dump Core IR.
226 pub dump_core: bool,
227 /// Dump Core IR after specific passes.
228 pub dump_core_passes: Vec<String>,
229 /// Dump Tensor IR.
230 pub dump_tensor_ir: bool,
231 /// Dump Loop IR.
232 pub dump_loop_ir: bool,
233 /// Dump LLVM IR.
234 pub dump_llvm: bool,
235}
236
237/// Search path configuration for finding modules and libraries.
238#[derive(Clone, Debug, Default)]
239pub struct SearchPaths {
240 /// Paths to search for source modules.
241 pub module_paths: Vec<Utf8PathBuf>,
242 /// Paths to search for interface files (.bhi).
243 pub interface_paths: Vec<Utf8PathBuf>,
244 /// Paths to search for libraries.
245 pub library_paths: Vec<Utf8PathBuf>,
246}
247
248impl SearchPaths {
249 /// Create a new empty search path configuration.
250 #[must_use]
251 pub fn new() -> Self {
252 Self::default()
253 }
254
255 /// Add a module search path.
256 pub fn add_module_path(&mut self, path: impl Into<Utf8PathBuf>) {
257 self.module_paths.push(path.into());
258 }
259
260 /// Add an interface search path.
261 pub fn add_interface_path(&mut self, path: impl Into<Utf8PathBuf>) {
262 self.interface_paths.push(path.into());
263 }
264
265 /// Add a library search path.
266 pub fn add_library_path(&mut self, path: impl Into<Utf8PathBuf>) {
267 self.library_paths.push(path.into());
268 }
269}
270
271/// Errors that can occur during session operations.
272#[derive(Debug, thiserror::Error)]
273pub enum SessionError {
274 /// Configuration file not found.
275 #[error("configuration file not found: {0}")]
276 ConfigNotFound(Utf8PathBuf),
277 /// Invalid configuration file.
278 #[error("invalid configuration: {0}")]
279 InvalidConfig(String),
280 /// Target not supported.
281 #[error("unsupported target: {0}")]
282 UnsupportedTarget(String),
283 /// IO error.
284 #[error("IO error: {0}")]
285 Io(#[from] std::io::Error),
286}
287
288/// The compiler session holds all state for a compilation unit.
289///
290/// This is the primary type for managing compilation state and should be
291/// created at the start of compilation and passed through all phases.
292pub struct Session {
293 /// Compiler options.
294 pub options: Options,
295 /// Search paths for modules and libraries.
296 pub search_paths: SearchPaths,
297 /// Set of already-loaded module names (for cycle detection).
298 loaded_modules: RwLock<FxHashSet<String>>,
299 /// Working directory for the session.
300 working_dir: Utf8PathBuf,
301}
302
303impl Session {
304 /// Create a new session with the given options.
305 ///
306 /// # Errors
307 ///
308 /// Returns an error if the current working directory cannot be determined.
309 pub fn new(options: Options) -> Result<Self, SessionError> {
310 let working_dir = std::env::current_dir()
311 .map_err(SessionError::Io)?
312 .try_into()
313 .map_err(|e| SessionError::InvalidConfig(format!("invalid working dir: {e}")))?;
314
315 Ok(Self {
316 options,
317 search_paths: SearchPaths::default(),
318 loaded_modules: RwLock::new(FxHashSet::default()),
319 working_dir,
320 })
321 }
322
323 /// Create a new session with default options.
324 ///
325 /// # Errors
326 ///
327 /// Returns an error if the current working directory cannot be determined.
328 pub fn with_defaults() -> Result<Self, SessionError> {
329 Self::new(Options::default())
330 }
331
332 /// Get the working directory for this session.
333 #[must_use]
334 pub fn working_dir(&self) -> &Utf8Path {
335 &self.working_dir
336 }
337
338 /// Get the compilation profile.
339 #[must_use]
340 pub fn profile(&self) -> Profile {
341 self.options.profile
342 }
343
344 /// Check if a module has been loaded (for cycle detection).
345 #[must_use]
346 pub fn is_module_loaded(&self, name: &str) -> bool {
347 self.loaded_modules.read().contains(name)
348 }
349
350 /// Mark a module as loaded.
351 pub fn mark_module_loaded(&self, name: String) {
352 self.loaded_modules.write().insert(name);
353 }
354
355 /// Get the output path, computing a default if not specified.
356 #[must_use]
357 pub fn output_path(&self, input_name: &str) -> Utf8PathBuf {
358 if let Some(ref path) = self.options.output_path {
359 path.clone()
360 } else {
361 let stem = Utf8Path::new(input_name).file_stem().unwrap_or(input_name);
362 let ext = match self.options.output_type {
363 OutputType::Object => "o",
364 OutputType::StaticLib => "a",
365 OutputType::DynamicLib => {
366 if cfg!(target_os = "macos") {
367 "dylib"
368 } else if cfg!(target_os = "windows") {
369 "dll"
370 } else {
371 "so"
372 }
373 }
374 OutputType::Executable => "",
375 OutputType::LlvmIr => "ll",
376 OutputType::LlvmBitcode => "bc",
377 OutputType::Assembly => "s",
378 OutputType::Wasm => "wasm",
379 };
380 if ext.is_empty() {
381 Utf8PathBuf::from(stem)
382 } else {
383 Utf8PathBuf::from(format!("{stem}.{ext}"))
384 }
385 }
386 }
387}
388
389/// A shared, thread-safe reference to a session.
390pub type SessionRef = Arc<Session>;
391
392/// Create a shared session reference.
393pub fn create_session(options: Options) -> Result<SessionRef, SessionError> {
394 Ok(Arc::new(Session::new(options)?))
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_profile_properties() {
403 assert!(!Profile::Default.is_strict_by_default());
404 assert!(!Profile::Server.is_strict_by_default());
405 assert!(Profile::Numeric.is_strict_by_default());
406 assert!(Profile::Edge.is_strict_by_default());
407
408 assert!(Profile::Numeric.has_fusion_guarantees());
409 assert!(!Profile::Default.has_fusion_guarantees());
410 }
411
412 #[test]
413 fn test_session_module_tracking() {
414 let session = Session::with_defaults().unwrap();
415 assert!(!session.is_module_loaded("Data.List"));
416 session.mark_module_loaded("Data.List".to_string());
417 assert!(session.is_module_loaded("Data.List"));
418 }
419}