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