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