1pub 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#[cfg(not(docsrs))]
68static RUNTIME_LIB: &[u8] = include_bytes!(env!("SEQ_RUNTIME_LIB_PATH"));
69
70#[cfg(docsrs)]
71static RUNTIME_LIB: &[u8] = &[];
72
73const MIN_CLANG_VERSION: u32 = 15;
76
77static CLANG_VERSION_CHECKED: OnceLock<Result<u32, String>> = OnceLock::new();
80
81fn 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 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 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
143fn parse_clang_version(output: &str) -> Option<u32> {
145 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 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
165pub 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
175pub fn compile_file_with_config(
180 source_path: &Path,
181 output_path: &Path,
182 keep_ir: bool,
183 config: &CompilerConfig,
184) -> Result<(), String> {
185 let source = fs::read_to_string(source_path)
187 .map_err(|e| format!("Failed to read source file: {}", e))?;
188
189 let mut parser = Parser::new(&source);
191 let program = parser.parse()?;
192
193 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 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 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 program.fixup_union_types();
235
236 program.generate_constructors()?;
239
240 normalize::lower_literal_if_combinators(&mut program);
244
245 check_collisions(&program.words)?;
247
248 check_union_collisions(&program.unions)?;
250
251 if program.find_word("main").is_none() {
253 return Err("No main word defined".to_string());
254 }
255
256 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 let call_graph = call_graph::CallGraph::build(&program);
264
265 let mut type_checker = TypeChecker::new();
267 type_checker.set_call_graph(call_graph.clone());
268
269 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 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 let quotation_types = type_checker.take_quotation_types();
303 let statement_types = type_checker.take_statement_top_types();
305 let aux_max_depths = type_checker.take_aux_max_depths();
307 let quotation_aux_depths = type_checker.take_quotation_aux_depths();
309 let resolved_sugar = type_checker.take_resolved_sugar();
311
312 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 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()?;
341
342 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 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 .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 for lib_path in &config.library_paths {
374 clang.arg("-L").arg(lib_path);
375 }
376
377 for lib in &config.libraries {
379 clang.arg("-l").arg(lib);
380 }
381
382 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 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 if !keep_ir {
401 fs::remove_file(&ir_path).ok();
402 }
403
404 Ok(())
405}
406
407pub fn compile_to_ir(source: &str) -> Result<String, String> {
409 compile_to_ir_with_config(source, &CompilerConfig::default())
410}
411
412pub 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 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 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;