1pub mod ast;
27pub mod builtins;
28pub mod call_graph;
29pub mod capture_analysis;
30pub mod codegen;
31pub mod config;
32pub mod ffi;
33pub mod lint;
34pub mod parser;
35pub mod resolver;
36pub mod resource_lint;
37pub mod script;
38pub mod stdlib_embed;
39pub mod test_runner;
40pub mod typechecker;
41pub mod types;
42pub mod unification;
43
44pub use ast::Program;
45pub use codegen::CodeGen;
46pub use config::{CompilerConfig, ExternalBuiltin, OptimizationLevel};
47pub use lint::{LintConfig, LintDiagnostic, Linter, Severity};
48pub use parser::Parser;
49pub use resolver::{
50 ResolveResult, Resolver, check_collisions, check_union_collisions, find_stdlib,
51};
52pub use resource_lint::{ProgramResourceAnalyzer, ResourceAnalyzer};
53pub use typechecker::TypeChecker;
54pub use types::{Effect, StackType, Type};
55
56use std::fs;
57use std::io::Write;
58use std::path::Path;
59use std::process::Command;
60use std::sync::OnceLock;
61
62#[cfg(not(docsrs))]
65static RUNTIME_LIB: &[u8] = include_bytes!(env!("SEQ_RUNTIME_LIB_PATH"));
66
67#[cfg(docsrs)]
68static RUNTIME_LIB: &[u8] = &[];
69
70const MIN_CLANG_VERSION: u32 = 15;
73
74static CLANG_VERSION_CHECKED: OnceLock<Result<u32, String>> = OnceLock::new();
77
78fn check_clang_version() -> Result<u32, String> {
82 CLANG_VERSION_CHECKED
83 .get_or_init(|| {
84 let output = Command::new("clang")
85 .arg("--version")
86 .output()
87 .map_err(|e| {
88 format!(
89 "Failed to run clang: {}. \
90 Please install clang {} or later.",
91 e, MIN_CLANG_VERSION
92 )
93 })?;
94
95 if !output.status.success() {
96 let stderr = String::from_utf8_lossy(&output.stderr);
97 return Err(format!(
98 "clang --version failed with exit code {:?}: {}",
99 output.status.code(),
100 stderr
101 ));
102 }
103
104 let version_str = String::from_utf8_lossy(&output.stdout);
105
106 let version = parse_clang_version(&version_str).ok_or_else(|| {
111 format!(
112 "Could not parse clang version from: {}\n\
113 seqc requires clang {} or later (for opaque pointer support).",
114 version_str.lines().next().unwrap_or(&version_str),
115 MIN_CLANG_VERSION
116 )
117 })?;
118
119 let is_apple = version_str.contains("Apple clang");
122 let effective_min = if is_apple { 14 } else { MIN_CLANG_VERSION };
123
124 if version < effective_min {
125 return Err(format!(
126 "clang version {} detected, but seqc requires {} {} or later.\n\
127 The generated LLVM IR uses opaque pointers (requires LLVM 15+).\n\
128 Please upgrade your clang installation.",
129 version,
130 if is_apple { "Apple clang" } else { "clang" },
131 effective_min
132 ));
133 }
134
135 Ok(version)
136 })
137 .clone()
138}
139
140fn parse_clang_version(output: &str) -> Option<u32> {
142 for line in output.lines() {
145 if line.contains("clang version")
146 && let Some(idx) = line.find("version ")
147 {
148 let after_version = &line[idx + 8..];
149 let major: String = after_version
151 .chars()
152 .take_while(|c| c.is_ascii_digit())
153 .collect();
154 if !major.is_empty() {
155 return major.parse().ok();
156 }
157 }
158 }
159 None
160}
161
162pub fn compile_file(source_path: &Path, output_path: &Path, keep_ir: bool) -> Result<(), String> {
164 compile_file_with_config(
165 source_path,
166 output_path,
167 keep_ir,
168 &CompilerConfig::default(),
169 )
170}
171
172pub fn compile_file_with_config(
177 source_path: &Path,
178 output_path: &Path,
179 keep_ir: bool,
180 config: &CompilerConfig,
181) -> Result<(), String> {
182 let source = fs::read_to_string(source_path)
184 .map_err(|e| format!("Failed to read source file: {}", e))?;
185
186 let mut parser = Parser::new(&source);
188 let program = parser.parse()?;
189
190 let (mut program, ffi_includes) = if !program.includes.is_empty() {
192 let stdlib_path = find_stdlib();
193 let mut resolver = Resolver::new(stdlib_path);
194 let result = resolver.resolve(source_path, program)?;
195 (result.program, result.ffi_includes)
196 } else {
197 (program, Vec::new())
198 };
199
200 let mut ffi_bindings = ffi::FfiBindings::new();
202 for ffi_name in &ffi_includes {
203 let manifest_content = ffi::get_ffi_manifest(ffi_name)
204 .ok_or_else(|| format!("FFI manifest '{}' not found", ffi_name))?;
205 let manifest = ffi::FfiManifest::parse(manifest_content)?;
206 ffi_bindings.add_manifest(&manifest)?;
207 }
208
209 for manifest_path in &config.ffi_manifest_paths {
211 let manifest_content = fs::read_to_string(manifest_path).map_err(|e| {
212 format!(
213 "Failed to read FFI manifest '{}': {}",
214 manifest_path.display(),
215 e
216 )
217 })?;
218 let manifest = ffi::FfiManifest::parse(&manifest_content).map_err(|e| {
219 format!(
220 "Failed to parse FFI manifest '{}': {}",
221 manifest_path.display(),
222 e
223 )
224 })?;
225 ffi_bindings.add_manifest(&manifest)?;
226 }
227
228 program.fixup_union_types();
232
233 program.generate_constructors()?;
236
237 check_collisions(&program.words)?;
239
240 check_union_collisions(&program.unions)?;
242
243 if program.find_word("main").is_none() {
245 return Err("No main word defined".to_string());
246 }
247
248 let mut external_names = config.external_names();
251 external_names.extend(ffi_bindings.function_names());
252 program.validate_word_calls_with_externals(&external_names)?;
253
254 let call_graph = call_graph::CallGraph::build(&program);
256
257 let mut type_checker = TypeChecker::new();
259 type_checker.set_call_graph(call_graph.clone());
260
261 if !config.external_builtins.is_empty() {
264 for builtin in &config.external_builtins {
265 if builtin.effect.is_none() {
266 return Err(format!(
267 "External builtin '{}' is missing a stack effect declaration.\n\
268 All external builtins must have explicit effects for type safety.",
269 builtin.seq_name
270 ));
271 }
272 }
273 let external_effects: Vec<(&str, &types::Effect)> = config
274 .external_builtins
275 .iter()
276 .map(|b| (b.seq_name.as_str(), b.effect.as_ref().unwrap()))
277 .collect();
278 type_checker.register_external_words(&external_effects);
279 }
280
281 if !ffi_bindings.functions.is_empty() {
283 let ffi_effects: Vec<(&str, &types::Effect)> = ffi_bindings
284 .functions
285 .values()
286 .map(|f| (f.seq_name.as_str(), &f.effect))
287 .collect();
288 type_checker.register_external_words(&ffi_effects);
289 }
290
291 type_checker.check_program(&program)?;
292
293 let quotation_types = type_checker.take_quotation_types();
295 let statement_types = type_checker.take_statement_top_types();
297 let aux_max_depths = type_checker.take_aux_max_depths();
299
300 let mut codegen = if config.pure_inline_test {
305 CodeGen::new_pure_inline_test()
306 } else {
307 CodeGen::new()
308 };
309 codegen.set_aux_slot_counts(aux_max_depths);
310 let ir = codegen
311 .codegen_program_with_ffi(
312 &program,
313 quotation_types,
314 statement_types,
315 config,
316 &ffi_bindings,
317 )
318 .map_err(|e| e.to_string())?;
319
320 let ir_path = output_path.with_extension("ll");
322 fs::write(&ir_path, ir).map_err(|e| format!("Failed to write IR file: {}", e))?;
323
324 check_clang_version()?;
326
327 let runtime_path = std::env::temp_dir().join("libseq_runtime.a");
329 {
330 let mut file = fs::File::create(&runtime_path)
331 .map_err(|e| format!("Failed to create runtime lib: {}", e))?;
332 file.write_all(RUNTIME_LIB)
333 .map_err(|e| format!("Failed to write runtime lib: {}", e))?;
334 }
335
336 let opt_flag = match config.optimization_level {
338 config::OptimizationLevel::O0 => "-O0",
339 config::OptimizationLevel::O1 => "-O1",
340 config::OptimizationLevel::O2 => "-O2",
341 config::OptimizationLevel::O3 => "-O3",
342 };
343 let mut clang = Command::new("clang");
344 clang
345 .arg(opt_flag)
346 .arg(&ir_path)
347 .arg("-o")
348 .arg(output_path)
349 .arg("-L")
350 .arg(runtime_path.parent().unwrap())
351 .arg("-lseq_runtime");
352
353 for lib_path in &config.library_paths {
355 clang.arg("-L").arg(lib_path);
356 }
357
358 for lib in &config.libraries {
360 clang.arg("-l").arg(lib);
361 }
362
363 for lib in &ffi_bindings.linker_flags {
365 clang.arg("-l").arg(lib);
366 }
367
368 let output = clang
369 .output()
370 .map_err(|e| format!("Failed to run clang: {}", e))?;
371
372 fs::remove_file(&runtime_path).ok();
374
375 if !output.status.success() {
376 let stderr = String::from_utf8_lossy(&output.stderr);
377 return Err(format!("Clang compilation failed:\n{}", stderr));
378 }
379
380 if !keep_ir {
382 fs::remove_file(&ir_path).ok();
383 }
384
385 Ok(())
386}
387
388pub fn compile_to_ir(source: &str) -> Result<String, String> {
390 compile_to_ir_with_config(source, &CompilerConfig::default())
391}
392
393pub fn compile_to_ir_with_config(source: &str, config: &CompilerConfig) -> Result<String, String> {
395 let mut parser = Parser::new(source);
396 let mut program = parser.parse()?;
397
398 if !program.unions.is_empty() {
400 program.generate_constructors()?;
401 }
402
403 let external_names = config.external_names();
404 program.validate_word_calls_with_externals(&external_names)?;
405
406 let mut type_checker = TypeChecker::new();
407
408 if !config.external_builtins.is_empty() {
411 for builtin in &config.external_builtins {
412 if builtin.effect.is_none() {
413 return Err(format!(
414 "External builtin '{}' is missing a stack effect declaration.\n\
415 All external builtins must have explicit effects for type safety.",
416 builtin.seq_name
417 ));
418 }
419 }
420 let external_effects: Vec<(&str, &types::Effect)> = config
421 .external_builtins
422 .iter()
423 .map(|b| (b.seq_name.as_str(), b.effect.as_ref().unwrap()))
424 .collect();
425 type_checker.register_external_words(&external_effects);
426 }
427
428 type_checker.check_program(&program)?;
429
430 let quotation_types = type_checker.take_quotation_types();
431 let statement_types = type_checker.take_statement_top_types();
432 let aux_max_depths = type_checker.take_aux_max_depths();
433
434 let mut codegen = CodeGen::new();
435 codegen.set_aux_slot_counts(aux_max_depths);
436 codegen
437 .codegen_program_with_config(&program, quotation_types, statement_types, config)
438 .map_err(|e| e.to_string())
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn test_parse_clang_version_standard() {
447 let output = "clang version 15.0.0 (https://github.com/llvm/llvm-project)\nTarget: x86_64";
448 assert_eq!(parse_clang_version(output), Some(15));
449 }
450
451 #[test]
452 fn test_parse_clang_version_apple() {
453 let output =
454 "Apple clang version 14.0.3 (clang-1403.0.22.14.1)\nTarget: arm64-apple-darwin";
455 assert_eq!(parse_clang_version(output), Some(14));
456 }
457
458 #[test]
459 fn test_parse_clang_version_homebrew() {
460 let output = "Homebrew clang version 17.0.6\nTarget: arm64-apple-darwin23.0.0";
461 assert_eq!(parse_clang_version(output), Some(17));
462 }
463
464 #[test]
465 fn test_parse_clang_version_ubuntu() {
466 let output = "Ubuntu clang version 15.0.7\nTarget: x86_64-pc-linux-gnu";
467 assert_eq!(parse_clang_version(output), Some(15));
468 }
469
470 #[test]
471 fn test_parse_clang_version_invalid() {
472 assert_eq!(parse_clang_version("no version here"), None);
473 assert_eq!(parse_clang_version("version "), None);
474 }
475}