seqc/
lib.rs

1//! Seq Compiler Library
2//!
3//! Provides compilation from .seq source to LLVM IR and executable binaries.
4//!
5//! # Extending the Compiler
6//!
7//! External projects can extend the compiler with additional builtins using
8//! [`CompilerConfig`]:
9//!
10//! ```rust,ignore
11//! use seqc::{CompilerConfig, ExternalBuiltin, compile_file_with_config};
12//!
13//! let config = CompilerConfig::new()
14//!     .with_builtin(ExternalBuiltin::new("my-op", "my_runtime_op"));
15//!
16//! compile_file_with_config(source, output, false, &config)?;
17//! ```
18
19pub mod ast;
20pub mod builtins;
21pub mod capture_analysis;
22pub mod codegen;
23pub mod config;
24pub mod ffi;
25pub mod lint;
26pub mod parser;
27pub mod resolver;
28pub mod resource_lint;
29pub mod stdlib_embed;
30pub mod test_runner;
31pub mod typechecker;
32pub mod types;
33pub mod unification;
34
35pub use ast::Program;
36pub use codegen::CodeGen;
37pub use config::{CompilerConfig, ExternalBuiltin};
38pub use lint::{LintConfig, LintDiagnostic, Linter, Severity};
39pub use parser::Parser;
40pub use resolver::{
41    ResolveResult, Resolver, check_collisions, check_union_collisions, find_stdlib,
42};
43pub use resource_lint::{ProgramResourceAnalyzer, ResourceAnalyzer};
44pub use typechecker::TypeChecker;
45pub use types::{Effect, StackType, Type};
46
47use std::fs;
48use std::io::Write;
49use std::path::Path;
50use std::process::Command;
51use std::sync::OnceLock;
52
53/// Embedded runtime library (built by build.rs)
54/// On docs.rs, this is an empty slice since the runtime isn't available.
55#[cfg(not(docsrs))]
56static RUNTIME_LIB: &[u8] = include_bytes!(env!("SEQ_RUNTIME_LIB_PATH"));
57
58#[cfg(docsrs)]
59static RUNTIME_LIB: &[u8] = &[];
60
61/// Minimum clang/LLVM version required.
62/// Our generated IR uses opaque pointers (`ptr`), which requires LLVM 15+.
63const MIN_CLANG_VERSION: u32 = 15;
64
65/// Cache for clang version check result.
66/// Stores Ok(version) on success or Err(message) on failure.
67static CLANG_VERSION_CHECKED: OnceLock<Result<u32, String>> = OnceLock::new();
68
69/// Check that clang is available and meets minimum version requirements.
70/// Returns Ok(version) on success, Err with helpful message on failure.
71/// This check is cached - it only runs once per process.
72fn check_clang_version() -> Result<u32, String> {
73    CLANG_VERSION_CHECKED
74        .get_or_init(|| {
75            let output = Command::new("clang")
76                .arg("--version")
77                .output()
78                .map_err(|e| {
79                    format!(
80                        "Failed to run clang: {}. \
81                         Please install clang {} or later.",
82                        e, MIN_CLANG_VERSION
83                    )
84                })?;
85
86            if !output.status.success() {
87                let stderr = String::from_utf8_lossy(&output.stderr);
88                return Err(format!(
89                    "clang --version failed with exit code {:?}: {}",
90                    output.status.code(),
91                    stderr
92                ));
93            }
94
95            let version_str = String::from_utf8_lossy(&output.stdout);
96
97            // Parse version from output like:
98            // "clang version 15.0.0 (...)"
99            // "Apple clang version 14.0.3 (...)"  (Apple's versioning differs)
100            // "Homebrew clang version 17.0.6"
101            let version = parse_clang_version(&version_str).ok_or_else(|| {
102                format!(
103                    "Could not parse clang version from: {}\n\
104                     seqc requires clang {} or later (for opaque pointer support).",
105                    version_str.lines().next().unwrap_or(&version_str),
106                    MIN_CLANG_VERSION
107                )
108            })?;
109
110            // Apple clang uses different version numbers - Apple clang 14 is based on LLVM 15
111            // For simplicity, we check if it's Apple clang and adjust expectations
112            let is_apple = version_str.contains("Apple clang");
113            let effective_min = if is_apple { 14 } else { MIN_CLANG_VERSION };
114
115            if version < effective_min {
116                return Err(format!(
117                    "clang version {} detected, but seqc requires {} {} or later.\n\
118                     The generated LLVM IR uses opaque pointers (requires LLVM 15+).\n\
119                     Please upgrade your clang installation.",
120                    version,
121                    if is_apple { "Apple clang" } else { "clang" },
122                    effective_min
123                ));
124            }
125
126            Ok(version)
127        })
128        .clone()
129}
130
131/// Parse major version number from clang --version output
132fn parse_clang_version(output: &str) -> Option<u32> {
133    // Look for "clang version X.Y.Z" pattern to avoid false positives
134    // This handles: "clang version", "Apple clang version", "Homebrew clang version", etc.
135    for line in output.lines() {
136        if line.contains("clang version")
137            && let Some(idx) = line.find("version ")
138        {
139            let after_version = &line[idx + 8..];
140            // Extract the major version number
141            let major: String = after_version
142                .chars()
143                .take_while(|c| c.is_ascii_digit())
144                .collect();
145            if !major.is_empty() {
146                return major.parse().ok();
147            }
148        }
149    }
150    None
151}
152
153/// Compile a .seq source file to an executable
154pub fn compile_file(source_path: &Path, output_path: &Path, keep_ir: bool) -> Result<(), String> {
155    compile_file_with_config(
156        source_path,
157        output_path,
158        keep_ir,
159        &CompilerConfig::default(),
160    )
161}
162
163/// Compile a .seq source file to an executable with custom configuration
164///
165/// This allows external projects to extend the compiler with additional
166/// builtins and link against additional libraries.
167pub fn compile_file_with_config(
168    source_path: &Path,
169    output_path: &Path,
170    keep_ir: bool,
171    config: &CompilerConfig,
172) -> Result<(), String> {
173    // Read source file
174    let source = fs::read_to_string(source_path)
175        .map_err(|e| format!("Failed to read source file: {}", e))?;
176
177    // Parse
178    let mut parser = Parser::new(&source);
179    let program = parser.parse()?;
180
181    // Resolve includes (if any)
182    let (mut program, ffi_includes) = if !program.includes.is_empty() {
183        let stdlib_path = find_stdlib();
184        let mut resolver = Resolver::new(stdlib_path);
185        let result = resolver.resolve(source_path, program)?;
186        (result.program, result.ffi_includes)
187    } else {
188        (program, Vec::new())
189    };
190
191    // Process FFI includes (embedded manifests from `include ffi:*`)
192    let mut ffi_bindings = ffi::FfiBindings::new();
193    for ffi_name in &ffi_includes {
194        let manifest_content = ffi::get_ffi_manifest(ffi_name)
195            .ok_or_else(|| format!("FFI manifest '{}' not found", ffi_name))?;
196        let manifest = ffi::FfiManifest::parse(manifest_content)?;
197        ffi_bindings.add_manifest(&manifest)?;
198    }
199
200    // Load external FFI manifests from config (--ffi-manifest)
201    for manifest_path in &config.ffi_manifest_paths {
202        let manifest_content = fs::read_to_string(manifest_path).map_err(|e| {
203            format!(
204                "Failed to read FFI manifest '{}': {}",
205                manifest_path.display(),
206                e
207            )
208        })?;
209        let manifest = ffi::FfiManifest::parse(&manifest_content).map_err(|e| {
210            format!(
211                "Failed to parse FFI manifest '{}': {}",
212                manifest_path.display(),
213                e
214            )
215        })?;
216        ffi_bindings.add_manifest(&manifest)?;
217    }
218
219    // Generate constructor words for all union types (Make-VariantName)
220    // Always done here to consolidate constructor generation in one place
221    program.generate_constructors()?;
222
223    // Check for word name collisions
224    check_collisions(&program.words)?;
225
226    // Check for union name collisions across modules
227    check_union_collisions(&program.unions)?;
228
229    // Verify we have a main word
230    if program.find_word("main").is_none() {
231        return Err("No main word defined".to_string());
232    }
233
234    // Validate all word calls reference defined words or built-ins
235    // Include external builtins from config and FFI functions
236    let mut external_names = config.external_names();
237    external_names.extend(ffi_bindings.function_names());
238    program.validate_word_calls_with_externals(&external_names)?;
239
240    // Type check (validates stack effects, especially for conditionals)
241    let mut type_checker = TypeChecker::new();
242
243    // Register external builtins with the type checker
244    if !config.external_builtins.is_empty() {
245        let external_effects: Vec<(&str, Option<&types::Effect>)> = config
246            .external_builtins
247            .iter()
248            .map(|b| (b.seq_name.as_str(), b.effect.as_ref()))
249            .collect();
250        type_checker.register_external_words(&external_effects);
251    }
252
253    // Register FFI functions with the type checker
254    if !ffi_bindings.functions.is_empty() {
255        let ffi_effects: Vec<(&str, Option<&types::Effect>)> = ffi_bindings
256            .functions
257            .values()
258            .map(|f| (f.seq_name.as_str(), Some(&f.effect)))
259            .collect();
260        type_checker.register_external_words(&ffi_effects);
261    }
262
263    type_checker.check_program(&program)?;
264
265    // Extract inferred quotation types (in DFS traversal order)
266    let quotation_types = type_checker.take_quotation_types();
267    // Extract per-statement type info for optimization (Issue #186)
268    let statement_types = type_checker.take_statement_top_types();
269
270    // Generate LLVM IR with type information and external builtins
271    let mut codegen = if config.pure_inline_test {
272        CodeGen::new_pure_inline_test()
273    } else {
274        CodeGen::new()
275    };
276    let ir = codegen
277        .codegen_program_with_ffi(
278            &program,
279            quotation_types,
280            statement_types,
281            config,
282            &ffi_bindings,
283        )
284        .map_err(|e| e.to_string())?;
285
286    // Write IR to file
287    let ir_path = output_path.with_extension("ll");
288    fs::write(&ir_path, ir).map_err(|e| format!("Failed to write IR file: {}", e))?;
289
290    // Check clang version before attempting to compile
291    check_clang_version()?;
292
293    // Extract embedded runtime library to a temp file
294    let runtime_path = std::env::temp_dir().join("libseq_runtime.a");
295    {
296        let mut file = fs::File::create(&runtime_path)
297            .map_err(|e| format!("Failed to create runtime lib: {}", e))?;
298        file.write_all(RUNTIME_LIB)
299            .map_err(|e| format!("Failed to write runtime lib: {}", e))?;
300    }
301
302    // Build clang command with library paths
303    let mut clang = Command::new("clang");
304    clang
305        .arg("-O3") // Enable aggressive LLVM optimizations
306        .arg(&ir_path)
307        .arg("-o")
308        .arg(output_path)
309        .arg("-L")
310        .arg(runtime_path.parent().unwrap())
311        .arg("-lseq_runtime");
312
313    // Add custom library paths from config
314    for lib_path in &config.library_paths {
315        clang.arg("-L").arg(lib_path);
316    }
317
318    // Add custom libraries from config
319    for lib in &config.libraries {
320        clang.arg("-l").arg(lib);
321    }
322
323    // Add FFI linker flags
324    for lib in &ffi_bindings.linker_flags {
325        clang.arg("-l").arg(lib);
326    }
327
328    let output = clang
329        .output()
330        .map_err(|e| format!("Failed to run clang: {}", e))?;
331
332    // Clean up temp runtime lib
333    fs::remove_file(&runtime_path).ok();
334
335    if !output.status.success() {
336        let stderr = String::from_utf8_lossy(&output.stderr);
337        return Err(format!("Clang compilation failed:\n{}", stderr));
338    }
339
340    // Remove temporary IR file unless user wants to keep it
341    if !keep_ir {
342        fs::remove_file(&ir_path).ok();
343    }
344
345    Ok(())
346}
347
348/// Compile source string to LLVM IR string (for testing)
349pub fn compile_to_ir(source: &str) -> Result<String, String> {
350    compile_to_ir_with_config(source, &CompilerConfig::default())
351}
352
353/// Compile source string to LLVM IR string with custom configuration
354pub fn compile_to_ir_with_config(source: &str, config: &CompilerConfig) -> Result<String, String> {
355    let mut parser = Parser::new(source);
356    let mut program = parser.parse()?;
357
358    // Generate constructors for unions
359    if !program.unions.is_empty() {
360        program.generate_constructors()?;
361    }
362
363    let external_names = config.external_names();
364    program.validate_word_calls_with_externals(&external_names)?;
365
366    let mut type_checker = TypeChecker::new();
367
368    // Register external builtins with the type checker
369    if !config.external_builtins.is_empty() {
370        let external_effects: Vec<(&str, Option<&types::Effect>)> = config
371            .external_builtins
372            .iter()
373            .map(|b| (b.seq_name.as_str(), b.effect.as_ref()))
374            .collect();
375        type_checker.register_external_words(&external_effects);
376    }
377
378    type_checker.check_program(&program)?;
379
380    let quotation_types = type_checker.take_quotation_types();
381    let statement_types = type_checker.take_statement_top_types();
382
383    let mut codegen = CodeGen::new();
384    codegen
385        .codegen_program_with_config(&program, quotation_types, statement_types, config)
386        .map_err(|e| e.to_string())
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn test_parse_clang_version_standard() {
395        let output = "clang version 15.0.0 (https://github.com/llvm/llvm-project)\nTarget: x86_64";
396        assert_eq!(parse_clang_version(output), Some(15));
397    }
398
399    #[test]
400    fn test_parse_clang_version_apple() {
401        let output =
402            "Apple clang version 14.0.3 (clang-1403.0.22.14.1)\nTarget: arm64-apple-darwin";
403        assert_eq!(parse_clang_version(output), Some(14));
404    }
405
406    #[test]
407    fn test_parse_clang_version_homebrew() {
408        let output = "Homebrew clang version 17.0.6\nTarget: arm64-apple-darwin23.0.0";
409        assert_eq!(parse_clang_version(output), Some(17));
410    }
411
412    #[test]
413    fn test_parse_clang_version_ubuntu() {
414        let output = "Ubuntu clang version 15.0.7\nTarget: x86_64-pc-linux-gnu";
415        assert_eq!(parse_clang_version(output), Some(15));
416    }
417
418    #[test]
419    fn test_parse_clang_version_invalid() {
420        assert_eq!(parse_clang_version("no version here"), None);
421        assert_eq!(parse_clang_version("version "), None);
422    }
423}