1pub mod ast;
27pub mod builtins;
28pub mod call_graph;
29pub mod capture_analysis;
30pub mod chan_yield_lint;
31pub mod codegen;
32pub mod config;
33pub mod error_flag_lint;
34pub mod ffi;
35pub mod lint;
36pub mod normalize;
37pub mod parser;
38pub mod resolver;
39pub mod resource_lint;
40pub mod script;
41pub mod stdlib_embed;
42pub mod test_runner;
43pub mod typechecker;
44pub mod types;
45pub mod unification;
46
47pub use ast::Program;
48pub use chan_yield_lint::ChanYieldAnalyzer;
49pub use codegen::CodeGen;
50pub use config::{CompilerConfig, ExternalBuiltin, OptimizationLevel};
51pub use error_flag_lint::ErrorFlagAnalyzer;
52pub use lint::{LintConfig, LintDiagnostic, Linter, Severity};
53pub use parser::Parser;
54pub use resolver::{
55 ResolveResult, Resolver, check_collisions, check_union_collisions, find_stdlib,
56};
57pub use resource_lint::{ProgramResourceAnalyzer, ResourceAnalyzer};
58pub use typechecker::TypeChecker;
59pub use types::{Effect, StackType, Type};
60
61use std::fs;
62use std::io::Write;
63use std::path::Path;
64use std::process::Command;
65use std::sync::OnceLock;
66
67#[cfg(not(docsrs))]
70static RUNTIME_LIB: &[u8] = include_bytes!(env!("SEQ_RUNTIME_LIB_PATH"));
71
72#[cfg(docsrs)]
73static RUNTIME_LIB: &[u8] = &[];
74
75const MIN_CLANG_VERSION: u32 = 15;
78
79static CLANG_VERSION_CHECKED: OnceLock<Result<u32, String>> = OnceLock::new();
82
83fn check_clang_version() -> Result<u32, String> {
87 CLANG_VERSION_CHECKED
88 .get_or_init(|| {
89 let output = Command::new("clang")
90 .arg("--version")
91 .output()
92 .map_err(|e| {
93 format!(
94 "Failed to run clang: {}. \
95 Please install clang {} or later.",
96 e, MIN_CLANG_VERSION
97 )
98 })?;
99
100 if !output.status.success() {
101 let stderr = String::from_utf8_lossy(&output.stderr);
102 return Err(format!(
103 "clang --version failed with exit code {:?}: {}",
104 output.status.code(),
105 stderr
106 ));
107 }
108
109 let version_str = String::from_utf8_lossy(&output.stdout);
110
111 let version = parse_clang_version(&version_str).ok_or_else(|| {
116 format!(
117 "Could not parse clang version from: {}\n\
118 seqc requires clang {} or later (for opaque pointer support).",
119 version_str.lines().next().unwrap_or(&version_str),
120 MIN_CLANG_VERSION
121 )
122 })?;
123
124 let is_apple = version_str.contains("Apple clang");
127 let effective_min = if is_apple { 14 } else { MIN_CLANG_VERSION };
128
129 if version < effective_min {
130 return Err(format!(
131 "clang version {} detected, but seqc requires {} {} or later.\n\
132 The generated LLVM IR uses opaque pointers (requires LLVM 15+).\n\
133 Please upgrade your clang installation.",
134 version,
135 if is_apple { "Apple clang" } else { "clang" },
136 effective_min
137 ));
138 }
139
140 Ok(version)
141 })
142 .clone()
143}
144
145fn parse_clang_version(output: &str) -> Option<u32> {
147 for line in output.lines() {
150 if line.contains("clang version")
151 && let Some(idx) = line.find("version ")
152 {
153 let after_version = &line[idx + 8..];
154 let major: String = after_version
156 .chars()
157 .take_while(|c| c.is_ascii_digit())
158 .collect();
159 if !major.is_empty() {
160 return major.parse().ok();
161 }
162 }
163 }
164 None
165}
166
167pub fn compile_file(source_path: &Path, output_path: &Path, keep_ir: bool) -> Result<(), String> {
169 compile_file_with_config(
170 source_path,
171 output_path,
172 keep_ir,
173 &CompilerConfig::default(),
174 )
175}
176
177pub fn compile_file_with_config(
182 source_path: &Path,
183 output_path: &Path,
184 keep_ir: bool,
185 config: &CompilerConfig,
186) -> Result<(), String> {
187 let source = fs::read_to_string(source_path)
189 .map_err(|e| format!("Failed to read source file: {}", e))?;
190
191 let mut parser = Parser::new(&source);
193 let program = parser.parse()?;
194
195 let (mut program, ffi_includes) = if !program.includes.is_empty() {
197 let stdlib_path = find_stdlib();
198 let mut resolver = Resolver::new(stdlib_path);
199 let result = resolver.resolve(source_path, program)?;
200 (result.program, result.ffi_includes)
201 } else {
202 (program, Vec::new())
203 };
204
205 let mut ffi_bindings = ffi::FfiBindings::new();
207 for ffi_name in &ffi_includes {
208 let manifest_content = ffi::get_ffi_manifest(ffi_name)
209 .ok_or_else(|| format!("FFI manifest '{}' not found", ffi_name))?;
210 let manifest = ffi::FfiManifest::parse(manifest_content)?;
211 ffi_bindings.add_manifest(&manifest)?;
212 }
213
214 for manifest_path in &config.ffi_manifest_paths {
216 let manifest_content = fs::read_to_string(manifest_path).map_err(|e| {
217 format!(
218 "Failed to read FFI manifest '{}': {}",
219 manifest_path.display(),
220 e
221 )
222 })?;
223 let manifest = ffi::FfiManifest::parse(&manifest_content).map_err(|e| {
224 format!(
225 "Failed to parse FFI manifest '{}': {}",
226 manifest_path.display(),
227 e
228 )
229 })?;
230 ffi_bindings.add_manifest(&manifest)?;
231 }
232
233 program.fixup_union_types();
237
238 program.generate_constructors()?;
241
242 normalize::lower_literal_if_combinators(&mut program);
246
247 check_collisions(&program.words)?;
249
250 check_union_collisions(&program.unions)?;
252
253 if program.find_word("main").is_none() {
255 return Err("No main word defined".to_string());
256 }
257
258 let mut external_names = config.external_names();
261 external_names.extend(ffi_bindings.function_names());
262 program.validate_word_calls_with_externals(&external_names)?;
263
264 let call_graph = call_graph::CallGraph::build(&program);
266
267 let mut type_checker = TypeChecker::new();
269 type_checker.set_call_graph(call_graph.clone());
270
271 if !config.external_builtins.is_empty() {
274 for builtin in &config.external_builtins {
275 if builtin.effect.is_none() {
276 return Err(format!(
277 "External builtin '{}' is missing a stack effect declaration.\n\
278 All external builtins must have explicit effects for type safety.",
279 builtin.seq_name
280 ));
281 }
282 }
283 let external_effects: Vec<(&str, &types::Effect)> = config
284 .external_builtins
285 .iter()
286 .map(|b| (b.seq_name.as_str(), b.effect.as_ref().unwrap()))
287 .collect();
288 type_checker.register_external_words(&external_effects);
289 }
290
291 if !ffi_bindings.functions.is_empty() {
293 let ffi_effects: Vec<(&str, &types::Effect)> = ffi_bindings
294 .functions
295 .values()
296 .map(|f| (f.seq_name.as_str(), &f.effect))
297 .collect();
298 type_checker.register_external_words(&ffi_effects);
299 }
300
301 type_checker.check_program(&program)?;
302
303 let quotation_types = type_checker.take_quotation_types();
305 let statement_types = type_checker.take_statement_top_types();
307 let aux_max_depths = type_checker.take_aux_max_depths();
309 let quotation_aux_depths = type_checker.take_quotation_aux_depths();
311 let resolved_sugar = type_checker.take_resolved_sugar();
313
314 let mut codegen = if config.pure_inline_test {
319 CodeGen::new_pure_inline_test()
320 } else {
321 CodeGen::new()
322 };
323 codegen.set_aux_slot_counts(aux_max_depths);
324 codegen.set_quotation_aux_slot_counts(quotation_aux_depths);
325 codegen.set_resolved_sugar(resolved_sugar);
326 codegen.set_source_file(source_path.to_path_buf());
327 codegen.set_call_graph(call_graph);
328 let ir = codegen
329 .codegen_program_with_ffi(
330 &program,
331 quotation_types,
332 statement_types,
333 config,
334 &ffi_bindings,
335 )
336 .map_err(|e| e.to_string())?;
337
338 let ir_path = output_path.with_extension("ll");
340 fs::write(&ir_path, ir).map_err(|e| format!("Failed to write IR file: {}", e))?;
341
342 check_clang_version()?;
344
345 let runtime_path = std::env::temp_dir().join("libseq_runtime.a");
347 {
348 let mut file = fs::File::create(&runtime_path)
349 .map_err(|e| format!("Failed to create runtime lib: {}", e))?;
350 file.write_all(RUNTIME_LIB)
351 .map_err(|e| format!("Failed to write runtime lib: {}", e))?;
352 }
353
354 let opt_flag = match config.optimization_level {
356 config::OptimizationLevel::O0 => "-O0",
357 config::OptimizationLevel::O1 => "-O1",
358 config::OptimizationLevel::O2 => "-O2",
359 config::OptimizationLevel::O3 => "-O3",
360 };
361 let mut clang = Command::new("clang");
362 clang
363 .arg(opt_flag)
364 .arg("-g")
368 .arg(&ir_path)
369 .arg("-o")
370 .arg(output_path)
371 .arg("-L")
372 .arg(runtime_path.parent().unwrap())
373 .arg("-lseq_runtime")
374 .arg("-lm");
380
381 for lib_path in &config.library_paths {
383 clang.arg("-L").arg(lib_path);
384 }
385
386 for lib in &config.libraries {
388 clang.arg("-l").arg(lib);
389 }
390
391 for lib in &ffi_bindings.linker_flags {
393 clang.arg("-l").arg(lib);
394 }
395
396 if cfg!(target_os = "macos") {
404 clang.arg("-Wl,-dead_strip");
405 } else if cfg!(target_os = "linux") {
406 clang.arg("-Wl,--gc-sections");
407 }
408
409 let output = clang
410 .output()
411 .map_err(|e| format!("Failed to run clang: {}", e))?;
412
413 fs::remove_file(&runtime_path).ok();
415
416 if !output.status.success() {
417 let stderr = String::from_utf8_lossy(&output.stderr);
418 return Err(format!("Clang compilation failed:\n{}", stderr));
419 }
420
421 if !keep_ir {
423 fs::remove_file(&ir_path).ok();
424 }
425
426 Ok(())
427}
428
429pub fn compile_to_ir(source: &str) -> Result<String, String> {
431 compile_to_ir_with_config(source, &CompilerConfig::default())
432}
433
434pub fn compile_to_ir_with_config(source: &str, config: &CompilerConfig) -> Result<String, String> {
436 let mut parser = Parser::new(source);
437 let mut program = parser.parse()?;
438
439 if !program.unions.is_empty() {
441 program.generate_constructors()?;
442 }
443
444 normalize::lower_literal_if_combinators(&mut program);
445
446 let external_names = config.external_names();
447 program.validate_word_calls_with_externals(&external_names)?;
448
449 let mut type_checker = TypeChecker::new();
450
451 if !config.external_builtins.is_empty() {
454 for builtin in &config.external_builtins {
455 if builtin.effect.is_none() {
456 return Err(format!(
457 "External builtin '{}' is missing a stack effect declaration.\n\
458 All external builtins must have explicit effects for type safety.",
459 builtin.seq_name
460 ));
461 }
462 }
463 let external_effects: Vec<(&str, &types::Effect)> = config
464 .external_builtins
465 .iter()
466 .map(|b| (b.seq_name.as_str(), b.effect.as_ref().unwrap()))
467 .collect();
468 type_checker.register_external_words(&external_effects);
469 }
470
471 type_checker.check_program(&program)?;
472
473 let call_graph = call_graph::CallGraph::build(&program);
475
476 let quotation_types = type_checker.take_quotation_types();
477 let statement_types = type_checker.take_statement_top_types();
478 let aux_max_depths = type_checker.take_aux_max_depths();
479 let quotation_aux_depths = type_checker.take_quotation_aux_depths();
480 let resolved_sugar = type_checker.take_resolved_sugar();
481
482 let mut codegen = CodeGen::new();
483 codegen.set_aux_slot_counts(aux_max_depths);
484 codegen.set_quotation_aux_slot_counts(quotation_aux_depths);
485 codegen.set_resolved_sugar(resolved_sugar);
486 codegen.set_call_graph(call_graph);
487 codegen
488 .codegen_program_with_config(&program, quotation_types, statement_types, config)
489 .map_err(|e| e.to_string())
490}
491
492#[cfg(test)]
493#[path = "lib/tests.rs"]
494mod tests;