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