bhc-session 0.2.19

Compiler session state, options, and configuration for BHC
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! Compiler session state, options, and configuration for BHC.
//!
//! This crate provides the central session management for the BHC compiler,
//! including compilation options, diagnostic configuration, and global state
//! that persists throughout a compilation unit.
//!
//! # Overview
//!
//! The [`Session`] type is the primary entry point, holding all state needed
//! for a single compilation. It includes:
//!
//! - Compiler options and flags
//! - Target specification
//! - Diagnostic handler
//! - Search paths for modules and libraries
//!
//! # Profiles
//!
//! BHC supports multiple compilation profiles as specified in H26-SPEC:
//!
//! - **Default**: Standard lazy Haskell semantics
//! - **Server**: Optimized for concurrent server workloads
//! - **Numeric**: Strict-by-default with fusion guarantees
//! - **Edge**: Minimal runtime for embedded/WASM targets

#![warn(missing_docs)]

use camino::{Utf8Path, Utf8PathBuf};
use parking_lot::RwLock;
use rustc_hash::FxHashSet;
use serde::{Deserialize, Serialize};
use std::sync::Arc;

/// The compilation profile determines evaluation semantics and optimization strategy.
///
/// See H26-SPEC Section 4 for detailed profile specifications.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Profile {
    /// Standard lazy Haskell evaluation with GC-managed memory.
    #[default]
    Default,
    /// Optimized for server workloads: concurrency, bounded latency, observability.
    Server,
    /// Numeric computing: strict-by-default, unboxed, fusion guaranteed.
    Numeric,
    /// Minimal runtime footprint for embedded and WASM targets.
    Edge,
    /// Realtime profile: bounded GC pauses (<1ms), arena per-frame.
    Realtime,
    /// Bare-metal microcontrollers: no GC, static allocation only.
    /// Programs with escaping allocations are rejected at compile time.
    Embedded,
}

impl Profile {
    /// Returns true if this profile uses strict evaluation by default.
    #[must_use]
    pub const fn is_strict_by_default(self) -> bool {
        matches!(self, Self::Numeric | Self::Edge | Self::Embedded)
    }

    /// Returns true if fusion is guaranteed for this profile.
    #[must_use]
    pub const fn has_fusion_guarantees(self) -> bool {
        matches!(self, Self::Numeric)
    }

    /// Returns true if this profile requires escape analysis.
    ///
    /// For Embedded profile, programs with escaping allocations are rejected
    /// at compile time since there is no GC to manage memory.
    #[must_use]
    pub const fn requires_escape_analysis(self) -> bool {
        matches!(self, Self::Embedded)
    }

    /// Returns true if this profile has no garbage collector.
    #[must_use]
    pub const fn is_gc_free(self) -> bool {
        matches!(self, Self::Embedded)
    }
}

/// Optimization level for code generation.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum OptLevel {
    /// No optimizations (fastest compilation).
    None,
    /// Basic optimizations.
    #[default]
    Less,
    /// Standard optimizations (default).
    Default,
    /// Aggressive optimizations.
    Aggressive,
    /// Size-optimized output.
    Size,
    /// Aggressively size-optimized output.
    SizeMin,
}

/// Debug information level.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DebugInfo {
    /// No debug information.
    #[default]
    None,
    /// Line tables only.
    LineTablesOnly,
    /// Full debug information.
    Full,
}

/// Output type for compilation.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum OutputType {
    /// Object file (.o).
    #[default]
    Object,
    /// Static library (.a).
    StaticLib,
    /// Dynamic library (.so/.dylib/.dll).
    DynamicLib,
    /// Executable binary.
    Executable,
    /// LLVM IR (.ll).
    LlvmIr,
    /// LLVM bitcode (.bc).
    LlvmBitcode,
    /// Assembly (.s).
    Assembly,
    /// WebAssembly module (.wasm).
    Wasm,
}

/// Compiler options that can be set via CLI or configuration.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Options {
    /// The compilation profile to use.
    pub profile: Profile,
    /// Optimization level.
    pub opt_level: OptLevel,
    /// Debug information level.
    pub debug_info: DebugInfo,
    /// Output type.
    pub output_type: OutputType,
    /// Target triple (e.g., "x86_64-unknown-linux-gnu").
    pub target_triple: Option<String>,
    /// Output path for compiled artifacts.
    pub output_path: Option<Utf8PathBuf>,
    /// Search paths for module imports.
    pub import_paths: Vec<Utf8PathBuf>,
    /// Search paths for libraries.
    pub library_paths: Vec<Utf8PathBuf>,
    /// Libraries to link.
    pub libraries: Vec<String>,
    /// Enable all warnings.
    pub warn_all: bool,
    /// Treat warnings as errors.
    pub deny_warnings: bool,
    /// Generate kernel reports (Numeric profile).
    pub emit_kernel_report: bool,
    /// Explicitly enable the tensor fusion pipeline regardless of profile.
    /// The Numeric profile always fuses; this requests fusion otherwise.
    pub tensor_fusion: bool,
    /// Dump intermediate representations.
    pub dump_ir: IrDumpOptions,
    /// Path to the BHC standard library.
    /// Used for implicit Prelude loading and module resolution.
    pub stdlib_path: Option<Utf8PathBuf>,
    /// Hackage package dependencies as "name:version" pairs.
    /// Example: `["filepath:1.4.100.0", "directory:1.3.8.0"]`
    pub hackage_packages: Vec<String>,
    /// Compile-only mode: produce .o files without linking.
    pub compile_only: bool,
    /// Output directory for object files (used with compile_only).
    pub output_object_dir: Option<Utf8PathBuf>,
    /// Output directory for interface files (used with compile_only).
    pub output_interface_dir: Option<Utf8PathBuf>,
    /// Package database paths for dependency lookup.
    pub package_dbs: Vec<Utf8PathBuf>,
    /// Exposed dependency package IDs.
    pub package_ids: Vec<String>,
    /// Enabled language extensions (e.g., "OverloadedStrings").
    pub extensions: Vec<String>,
    /// CPP preprocessor defines (e.g., "FOO", "BAR=1").
    pub cpp_defines: Vec<String>,
}

impl Default for Options {
    fn default() -> Self {
        Self {
            profile: Profile::Default,
            opt_level: OptLevel::Default,
            debug_info: DebugInfo::None,
            output_type: OutputType::Object,
            target_triple: None,
            output_path: None,
            import_paths: Vec::new(),
            library_paths: Vec::new(),
            libraries: Vec::new(),
            warn_all: false,
            deny_warnings: false,
            emit_kernel_report: false,
            tensor_fusion: false,
            dump_ir: IrDumpOptions::default(),
            stdlib_path: None,
            hackage_packages: Vec::new(),
            compile_only: false,
            output_object_dir: None,
            output_interface_dir: None,
            package_dbs: Vec::new(),
            package_ids: Vec::new(),
            extensions: Vec::new(),
            cpp_defines: Vec::new(),
        }
    }
}

/// Options for dumping intermediate representations.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct IrDumpOptions {
    /// Dump AST after parsing.
    pub dump_ast: bool,
    /// Dump Core IR.
    pub dump_core: bool,
    /// Dump Core IR after specific passes.
    pub dump_core_passes: Vec<String>,
    /// Dump Tensor IR.
    pub dump_tensor_ir: bool,
    /// Dump Loop IR.
    pub dump_loop_ir: bool,
    /// Dump LLVM IR.
    pub dump_llvm: bool,
}

/// Search path configuration for finding modules and libraries.
#[derive(Clone, Debug, Default)]
pub struct SearchPaths {
    /// Paths to search for source modules.
    pub module_paths: Vec<Utf8PathBuf>,
    /// Paths to search for interface files (.bhi).
    pub interface_paths: Vec<Utf8PathBuf>,
    /// Paths to search for libraries.
    pub library_paths: Vec<Utf8PathBuf>,
}

impl SearchPaths {
    /// Create a new empty search path configuration.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a module search path.
    pub fn add_module_path(&mut self, path: impl Into<Utf8PathBuf>) {
        self.module_paths.push(path.into());
    }

    /// Add an interface search path.
    pub fn add_interface_path(&mut self, path: impl Into<Utf8PathBuf>) {
        self.interface_paths.push(path.into());
    }

    /// Add a library search path.
    pub fn add_library_path(&mut self, path: impl Into<Utf8PathBuf>) {
        self.library_paths.push(path.into());
    }
}

/// Errors that can occur during session operations.
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
    /// Configuration file not found.
    #[error("configuration file not found: {0}")]
    ConfigNotFound(Utf8PathBuf),
    /// Invalid configuration file.
    #[error("invalid configuration: {0}")]
    InvalidConfig(String),
    /// Target not supported.
    #[error("unsupported target: {0}")]
    UnsupportedTarget(String),
    /// IO error.
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

/// The compiler session holds all state for a compilation unit.
///
/// This is the primary type for managing compilation state and should be
/// created at the start of compilation and passed through all phases.
pub struct Session {
    /// Compiler options.
    pub options: Options,
    /// Search paths for modules and libraries.
    pub search_paths: SearchPaths,
    /// Set of already-loaded module names (for cycle detection).
    loaded_modules: RwLock<FxHashSet<String>>,
    /// Working directory for the session.
    working_dir: Utf8PathBuf,
}

impl Session {
    /// Create a new session with the given options.
    ///
    /// # Errors
    ///
    /// Returns an error if the current working directory cannot be determined.
    pub fn new(options: Options) -> Result<Self, SessionError> {
        let working_dir = std::env::current_dir()
            .map_err(SessionError::Io)?
            .try_into()
            .map_err(|e| SessionError::InvalidConfig(format!("invalid working dir: {e}")))?;

        Ok(Self {
            options,
            search_paths: SearchPaths::default(),
            loaded_modules: RwLock::new(FxHashSet::default()),
            working_dir,
        })
    }

    /// Create a new session with default options.
    ///
    /// # Errors
    ///
    /// Returns an error if the current working directory cannot be determined.
    pub fn with_defaults() -> Result<Self, SessionError> {
        Self::new(Options::default())
    }

    /// Get the working directory for this session.
    #[must_use]
    pub fn working_dir(&self) -> &Utf8Path {
        &self.working_dir
    }

    /// Get the compilation profile.
    #[must_use]
    pub fn profile(&self) -> Profile {
        self.options.profile
    }

    /// Check if a module has been loaded (for cycle detection).
    #[must_use]
    pub fn is_module_loaded(&self, name: &str) -> bool {
        self.loaded_modules.read().contains(name)
    }

    /// Mark a module as loaded.
    pub fn mark_module_loaded(&self, name: String) {
        self.loaded_modules.write().insert(name);
    }

    /// Get the output path, computing a default if not specified.
    #[must_use]
    pub fn output_path(&self, input_name: &str) -> Utf8PathBuf {
        if let Some(ref path) = self.options.output_path {
            path.clone()
        } else {
            let stem = Utf8Path::new(input_name).file_stem().unwrap_or(input_name);
            let ext = match self.options.output_type {
                OutputType::Object => "o",
                OutputType::StaticLib => "a",
                OutputType::DynamicLib => {
                    if cfg!(target_os = "macos") {
                        "dylib"
                    } else if cfg!(target_os = "windows") {
                        "dll"
                    } else {
                        "so"
                    }
                }
                OutputType::Executable => "",
                OutputType::LlvmIr => "ll",
                OutputType::LlvmBitcode => "bc",
                OutputType::Assembly => "s",
                OutputType::Wasm => "wasm",
            };
            if ext.is_empty() {
                Utf8PathBuf::from(stem)
            } else {
                Utf8PathBuf::from(format!("{stem}.{ext}"))
            }
        }
    }
}

/// A shared, thread-safe reference to a session.
pub type SessionRef = Arc<Session>;

/// Create a shared session reference.
pub fn create_session(options: Options) -> Result<SessionRef, SessionError> {
    Ok(Arc::new(Session::new(options)?))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_profile_properties() {
        assert!(!Profile::Default.is_strict_by_default());
        assert!(!Profile::Server.is_strict_by_default());
        assert!(Profile::Numeric.is_strict_by_default());
        assert!(Profile::Edge.is_strict_by_default());

        assert!(Profile::Numeric.has_fusion_guarantees());
        assert!(!Profile::Default.has_fusion_guarantees());
    }

    #[test]
    fn test_session_module_tracking() {
        let session = Session::with_defaults().unwrap();
        assert!(!session.is_module_loaded("Data.List"));
        session.mark_module_loaded("Data.List".to_string());
        assert!(session.is_module_loaded("Data.List"));
    }
}