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