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