Skip to main content

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