pub mod ast;
pub mod builtins;
pub mod call_graph;
pub mod capture_analysis;
pub mod codegen;
pub mod config;
pub mod ffi;
pub mod lint;
pub mod parser;
pub mod resolver;
pub mod resource_lint;
pub mod script;
pub mod stdlib_embed;
pub mod test_runner;
pub mod typechecker;
pub mod types;
pub mod unification;
pub use ast::Program;
pub use codegen::CodeGen;
pub use config::{CompilerConfig, ExternalBuiltin, OptimizationLevel};
pub use lint::{LintConfig, LintDiagnostic, Linter, Severity};
pub use parser::Parser;
pub use resolver::{
ResolveResult, Resolver, check_collisions, check_union_collisions, find_stdlib,
};
pub use resource_lint::{ProgramResourceAnalyzer, ResourceAnalyzer};
pub use typechecker::TypeChecker;
pub use types::{Effect, StackType, Type};
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::Command;
use std::sync::OnceLock;
#[cfg(not(docsrs))]
static RUNTIME_LIB: &[u8] = include_bytes!(env!("SEQ_RUNTIME_LIB_PATH"));
#[cfg(docsrs)]
static RUNTIME_LIB: &[u8] = &[];
const MIN_CLANG_VERSION: u32 = 15;
static CLANG_VERSION_CHECKED: OnceLock<Result<u32, String>> = OnceLock::new();
fn check_clang_version() -> Result<u32, String> {
CLANG_VERSION_CHECKED
.get_or_init(|| {
let output = Command::new("clang")
.arg("--version")
.output()
.map_err(|e| {
format!(
"Failed to run clang: {}. \
Please install clang {} or later.",
e, MIN_CLANG_VERSION
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"clang --version failed with exit code {:?}: {}",
output.status.code(),
stderr
));
}
let version_str = String::from_utf8_lossy(&output.stdout);
let version = parse_clang_version(&version_str).ok_or_else(|| {
format!(
"Could not parse clang version from: {}\n\
seqc requires clang {} or later (for opaque pointer support).",
version_str.lines().next().unwrap_or(&version_str),
MIN_CLANG_VERSION
)
})?;
let is_apple = version_str.contains("Apple clang");
let effective_min = if is_apple { 14 } else { MIN_CLANG_VERSION };
if version < effective_min {
return Err(format!(
"clang version {} detected, but seqc requires {} {} or later.\n\
The generated LLVM IR uses opaque pointers (requires LLVM 15+).\n\
Please upgrade your clang installation.",
version,
if is_apple { "Apple clang" } else { "clang" },
effective_min
));
}
Ok(version)
})
.clone()
}
fn parse_clang_version(output: &str) -> Option<u32> {
for line in output.lines() {
if line.contains("clang version")
&& let Some(idx) = line.find("version ")
{
let after_version = &line[idx + 8..];
let major: String = after_version
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
if !major.is_empty() {
return major.parse().ok();
}
}
}
None
}
pub fn compile_file(source_path: &Path, output_path: &Path, keep_ir: bool) -> Result<(), String> {
compile_file_with_config(
source_path,
output_path,
keep_ir,
&CompilerConfig::default(),
)
}
pub fn compile_file_with_config(
source_path: &Path,
output_path: &Path,
keep_ir: bool,
config: &CompilerConfig,
) -> Result<(), String> {
let source = fs::read_to_string(source_path)
.map_err(|e| format!("Failed to read source file: {}", e))?;
let mut parser = Parser::new(&source);
let program = parser.parse()?;
let (mut program, ffi_includes) = if !program.includes.is_empty() {
let stdlib_path = find_stdlib();
let mut resolver = Resolver::new(stdlib_path);
let result = resolver.resolve(source_path, program)?;
(result.program, result.ffi_includes)
} else {
(program, Vec::new())
};
let mut ffi_bindings = ffi::FfiBindings::new();
for ffi_name in &ffi_includes {
let manifest_content = ffi::get_ffi_manifest(ffi_name)
.ok_or_else(|| format!("FFI manifest '{}' not found", ffi_name))?;
let manifest = ffi::FfiManifest::parse(manifest_content)?;
ffi_bindings.add_manifest(&manifest)?;
}
for manifest_path in &config.ffi_manifest_paths {
let manifest_content = fs::read_to_string(manifest_path).map_err(|e| {
format!(
"Failed to read FFI manifest '{}': {}",
manifest_path.display(),
e
)
})?;
let manifest = ffi::FfiManifest::parse(&manifest_content).map_err(|e| {
format!(
"Failed to parse FFI manifest '{}': {}",
manifest_path.display(),
e
)
})?;
ffi_bindings.add_manifest(&manifest)?;
}
program.generate_constructors()?;
check_collisions(&program.words)?;
check_union_collisions(&program.unions)?;
if program.find_word("main").is_none() {
return Err("No main word defined".to_string());
}
let mut external_names = config.external_names();
external_names.extend(ffi_bindings.function_names());
program.validate_word_calls_with_externals(&external_names)?;
let call_graph = call_graph::CallGraph::build(&program);
let mut type_checker = TypeChecker::new();
type_checker.set_call_graph(call_graph.clone());
if !config.external_builtins.is_empty() {
for builtin in &config.external_builtins {
if builtin.effect.is_none() {
return Err(format!(
"External builtin '{}' is missing a stack effect declaration.\n\
All external builtins must have explicit effects for type safety.",
builtin.seq_name
));
}
}
let external_effects: Vec<(&str, &types::Effect)> = config
.external_builtins
.iter()
.map(|b| (b.seq_name.as_str(), b.effect.as_ref().unwrap()))
.collect();
type_checker.register_external_words(&external_effects);
}
if !ffi_bindings.functions.is_empty() {
let ffi_effects: Vec<(&str, &types::Effect)> = ffi_bindings
.functions
.values()
.map(|f| (f.seq_name.as_str(), &f.effect))
.collect();
type_checker.register_external_words(&ffi_effects);
}
type_checker.check_program(&program)?;
let quotation_types = type_checker.take_quotation_types();
let statement_types = type_checker.take_statement_top_types();
let mut codegen = if config.pure_inline_test {
CodeGen::new_pure_inline_test()
} else {
CodeGen::new()
};
let ir = codegen
.codegen_program_with_ffi(
&program,
quotation_types,
statement_types,
config,
&ffi_bindings,
)
.map_err(|e| e.to_string())?;
let ir_path = output_path.with_extension("ll");
fs::write(&ir_path, ir).map_err(|e| format!("Failed to write IR file: {}", e))?;
check_clang_version()?;
let runtime_path = std::env::temp_dir().join("libseq_runtime.a");
{
let mut file = fs::File::create(&runtime_path)
.map_err(|e| format!("Failed to create runtime lib: {}", e))?;
file.write_all(RUNTIME_LIB)
.map_err(|e| format!("Failed to write runtime lib: {}", e))?;
}
let opt_flag = match config.optimization_level {
config::OptimizationLevel::O0 => "-O0",
config::OptimizationLevel::O1 => "-O1",
config::OptimizationLevel::O2 => "-O2",
config::OptimizationLevel::O3 => "-O3",
};
let mut clang = Command::new("clang");
clang
.arg(opt_flag)
.arg(&ir_path)
.arg("-o")
.arg(output_path)
.arg("-L")
.arg(runtime_path.parent().unwrap())
.arg("-lseq_runtime");
for lib_path in &config.library_paths {
clang.arg("-L").arg(lib_path);
}
for lib in &config.libraries {
clang.arg("-l").arg(lib);
}
for lib in &ffi_bindings.linker_flags {
clang.arg("-l").arg(lib);
}
let output = clang
.output()
.map_err(|e| format!("Failed to run clang: {}", e))?;
fs::remove_file(&runtime_path).ok();
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Clang compilation failed:\n{}", stderr));
}
if !keep_ir {
fs::remove_file(&ir_path).ok();
}
Ok(())
}
pub fn compile_to_ir(source: &str) -> Result<String, String> {
compile_to_ir_with_config(source, &CompilerConfig::default())
}
pub fn compile_to_ir_with_config(source: &str, config: &CompilerConfig) -> Result<String, String> {
let mut parser = Parser::new(source);
let mut program = parser.parse()?;
if !program.unions.is_empty() {
program.generate_constructors()?;
}
let external_names = config.external_names();
program.validate_word_calls_with_externals(&external_names)?;
let mut type_checker = TypeChecker::new();
if !config.external_builtins.is_empty() {
for builtin in &config.external_builtins {
if builtin.effect.is_none() {
return Err(format!(
"External builtin '{}' is missing a stack effect declaration.\n\
All external builtins must have explicit effects for type safety.",
builtin.seq_name
));
}
}
let external_effects: Vec<(&str, &types::Effect)> = config
.external_builtins
.iter()
.map(|b| (b.seq_name.as_str(), b.effect.as_ref().unwrap()))
.collect();
type_checker.register_external_words(&external_effects);
}
type_checker.check_program(&program)?;
let quotation_types = type_checker.take_quotation_types();
let statement_types = type_checker.take_statement_top_types();
let mut codegen = CodeGen::new();
codegen
.codegen_program_with_config(&program, quotation_types, statement_types, config)
.map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_clang_version_standard() {
let output = "clang version 15.0.0 (https://github.com/llvm/llvm-project)\nTarget: x86_64";
assert_eq!(parse_clang_version(output), Some(15));
}
#[test]
fn test_parse_clang_version_apple() {
let output =
"Apple clang version 14.0.3 (clang-1403.0.22.14.1)\nTarget: arm64-apple-darwin";
assert_eq!(parse_clang_version(output), Some(14));
}
#[test]
fn test_parse_clang_version_homebrew() {
let output = "Homebrew clang version 17.0.6\nTarget: arm64-apple-darwin23.0.0";
assert_eq!(parse_clang_version(output), Some(17));
}
#[test]
fn test_parse_clang_version_ubuntu() {
let output = "Ubuntu clang version 15.0.7\nTarget: x86_64-pc-linux-gnu";
assert_eq!(parse_clang_version(output), Some(15));
}
#[test]
fn test_parse_clang_version_invalid() {
assert_eq!(parse_clang_version("no version here"), None);
assert_eq!(parse_clang_version("version "), None);
}
}