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