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