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