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