Skip to main content

logicaffeine_compile/
compile.rs

1//! LOGOS Compilation Pipeline
2//!
3//! This module provides the end-to-end compilation pipeline that transforms
4//! LOGOS source code into executable Rust programs.
5//!
6//! # Pipeline Overview
7//!
8//! ```text
9//! LOGOS Source (.md)
10//!       │
11//!       ▼
12//! ┌───────────────────┐
13//! │  1. Lexer         │ Tokenize source
14//! └─────────┬─────────┘
15//!           ▼
16//! ┌───────────────────┐
17//! │  2. Discovery     │ Type & policy definitions
18//! └─────────┬─────────┘
19//!           ▼
20//! ┌───────────────────┐
21//! │  3. Parser        │ Build AST
22//! └─────────┬─────────┘
23//!           ▼
24//! ┌───────────────────┐
25//! │  4. Analysis      │ Escape, ownership, verification
26//! └─────────┬─────────┘
27//!           ▼
28//! ┌───────────────────┐
29//! │  5. CodeGen       │ Emit Rust source
30//! └─────────┬─────────┘
31//!           ▼
32//!     Rust Source
33//! ```
34//!
35//! # Compilation Functions
36//!
37//! | Function | Analysis | Use Case |
38//! |----------|----------|----------|
39//! | [`compile_to_rust`] | Escape only | Basic compilation |
40//! | [`compile_to_rust_checked`] | Escape + Ownership | Use with `--check` flag |
41//! | `compile_to_rust_verified` | All + Z3 | Formal verification (requires `verification` feature) |
42//! | [`compile_project`] | Multi-file | Projects with imports |
43//! | [`compile_and_run`] | Full + Execute | Development workflow |
44//!
45//! # Examples
46//!
47//! ## Basic Compilation
48//!
49//! ```
50//! # use logicaffeine_compile::compile::compile_to_rust;
51//! # use logicaffeine_compile::ParseError;
52//! # fn main() -> Result<(), ParseError> {
53//! let source = "## Main\nLet x be 5.\nShow x.";
54//! let rust_code = compile_to_rust(source)?;
55//! // rust_code contains:
56//! // fn main() {
57//! //     let x = 5;
58//! //     println!("{}", x);
59//! // }
60//! # Ok(())
61//! # }
62//! ```
63//!
64//! ## With Ownership Checking
65//!
66//! ```
67//! # use logicaffeine_compile::compile::compile_to_rust_checked;
68//! let source = "## Main\nLet x be 5.\nGive x to y.\nShow x.";
69//! let result = compile_to_rust_checked(source);
70//! // Returns Err: "x has already been given away"
71//! ```
72
73use std::fs;
74use std::io::Write;
75use std::path::Path;
76use std::process::Command;
77
78// Runtime crates paths (relative to workspace root)
79const CRATES_DATA_PATH: &str = "crates/logicaffeine_data";
80const CRATES_SYSTEM_PATH: &str = "crates/logicaffeine_system";
81
82use std::fmt::Write as FmtWrite;
83
84use crate::analysis::{DiscoveryPass, EscapeChecker, OwnershipChecker, PolicyRegistry};
85use crate::arena::Arena;
86use crate::arena_ctx::AstContext;
87use crate::ast::{Expr, Stmt, TypeExpr};
88use crate::codegen::{codegen_program, generate_c_header, generate_python_bindings, generate_typescript_bindings};
89use crate::diagnostic::{parse_rustc_json, translate_diagnostics, LogosError};
90use crate::drs::WorldState;
91use crate::error::ParseError;
92use crate::intern::Interner;
93use crate::lexer::Lexer;
94use crate::parser::Parser;
95use crate::sourcemap::SourceMap;
96
97/// A declared external crate dependency from a `## Requires` block.
98#[derive(Debug, Clone)]
99pub struct CrateDependency {
100    pub name: String,
101    pub version: String,
102    pub features: Vec<String>,
103}
104
105/// Full compilation output including generated Rust code and extracted dependencies.
106#[derive(Debug)]
107pub struct CompileOutput {
108    pub rust_code: String,
109    pub dependencies: Vec<CrateDependency>,
110    /// Generated C header content (populated when C exports exist).
111    pub c_header: Option<String>,
112    /// Generated Python ctypes bindings (populated when C exports exist).
113    pub python_bindings: Option<String>,
114    /// Generated TypeScript type declarations (.d.ts content, populated when C exports exist).
115    pub typescript_types: Option<String>,
116    /// Generated TypeScript FFI bindings (.js content, populated when C exports exist).
117    pub typescript_bindings: Option<String>,
118}
119
120/// Compile LOGOS source to Rust source code.
121///
122/// This is the basic compilation function that runs lexing, parsing, and
123/// escape analysis before generating Rust code.
124///
125/// # Arguments
126///
127/// * `source` - LOGOS source code as a string
128///
129/// # Returns
130///
131/// Generated Rust source code on success.
132///
133/// # Errors
134///
135/// Returns [`ParseError`] if:
136/// - Lexical analysis fails (invalid tokens)
137/// - Parsing fails (syntax errors)
138/// - Escape analysis fails (zone-local values escaping)
139///
140/// # Example
141///
142/// ```
143/// # use logicaffeine_compile::compile::compile_to_rust;
144/// # use logicaffeine_compile::ParseError;
145/// # fn main() -> Result<(), ParseError> {
146/// let source = "## Main\nLet x be 5.\nShow x.";
147/// let rust_code = compile_to_rust(source)?;
148/// assert!(rust_code.contains("let x = 5;"));
149/// # Ok(())
150/// # }
151/// ```
152pub fn compile_to_rust(source: &str) -> Result<String, ParseError> {
153    compile_program_full(source).map(|o| o.rust_code)
154}
155
156/// Compile LOGOS source and return full output including dependency metadata.
157///
158/// This is the primary compilation entry point that returns both the generated
159/// Rust code and any crate dependencies declared in `## Requires` blocks.
160pub fn compile_program_full(source: &str) -> Result<CompileOutput, ParseError> {
161    let mut interner = Interner::new();
162    let mut lexer = Lexer::new(source, &mut interner);
163    let tokens = lexer.tokenize();
164
165    // Pass 1: Discovery - scan for type definitions and policies
166    let (type_registry, policy_registry) = {
167        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
168        let result = discovery.run_full();
169        (result.types, result.policies)
170    };
171    // Clone for codegen (parser takes ownership)
172    let codegen_registry = type_registry.clone();
173    let codegen_policies = policy_registry.clone();
174
175    let mut world_state = WorldState::new();
176    let expr_arena = Arena::new();
177    let term_arena = Arena::new();
178    let np_arena = Arena::new();
179    let sym_arena = Arena::new();
180    let role_arena = Arena::new();
181    let pp_arena = Arena::new();
182    let stmt_arena: Arena<Stmt> = Arena::new();
183    let imperative_expr_arena: Arena<Expr> = Arena::new();
184    let type_expr_arena: Arena<TypeExpr> = Arena::new();
185
186    let ast_ctx = AstContext::with_types(
187        &expr_arena,
188        &term_arena,
189        &np_arena,
190        &sym_arena,
191        &role_arena,
192        &pp_arena,
193        &stmt_arena,
194        &imperative_expr_arena,
195        &type_expr_arena,
196    );
197
198    // Pass 2: Parse with type context
199    let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ast_ctx, type_registry);
200    // Note: Don't call process_block_headers() - parse_program handles blocks itself
201
202    let stmts = parser.parse_program()?;
203
204    // Extract dependencies before escape analysis
205    let mut dependencies = extract_dependencies(&stmts, &interner)?;
206
207    // FFI: Auto-inject wasm-bindgen dependency if any function is exported for WASM
208    let needs_wasm_bindgen = stmts.iter().any(|stmt| {
209        if let Stmt::FunctionDef { is_exported: true, export_target: Some(target), .. } = stmt {
210            interner.resolve(*target).eq_ignore_ascii_case("wasm")
211        } else {
212            false
213        }
214    });
215    if needs_wasm_bindgen && !dependencies.iter().any(|d| d.name == "wasm-bindgen") {
216        dependencies.push(CrateDependency {
217            name: "wasm-bindgen".to_string(),
218            version: "0.2".to_string(),
219            features: vec![],
220        });
221    }
222
223    // Pass 3: Escape analysis - check for zone escape violations
224    // This catches obvious cases like returning zone-local variables
225    let mut escape_checker = EscapeChecker::new(&interner);
226    escape_checker.check_program(&stmts).map_err(|e| {
227        // Convert EscapeError to ParseError for now
228        // The error message is already Socratic from EscapeChecker
229        ParseError {
230            kind: crate::error::ParseErrorKind::Custom(e.to_string()),
231            span: e.span,
232        }
233    })?;
234
235    // Note: Static verification is available when the `verification` feature is enabled,
236    // but must be explicitly invoked via compile_to_rust_verified().
237
238    let rust_code = codegen_program(&stmts, &codegen_registry, &codegen_policies, &interner);
239
240    // Universal ABI: Generate C header + bindings if any C exports exist
241    let has_c = stmts.iter().any(|stmt| {
242        if let Stmt::FunctionDef { is_exported: true, export_target, .. } = stmt {
243            match export_target {
244                None => true,
245                Some(t) => interner.resolve(*t).eq_ignore_ascii_case("c"),
246            }
247        } else {
248            false
249        }
250    });
251
252    let c_header = if has_c {
253        Some(generate_c_header(&stmts, "module", &interner, &codegen_registry))
254    } else {
255        None
256    };
257
258    // Auto-inject serde_json dependency when C exports exist (needed for collection to_json and portable struct JSON accessors)
259    if has_c && !dependencies.iter().any(|d| d.name == "serde_json") {
260        dependencies.push(CrateDependency {
261            name: "serde_json".to_string(),
262            version: "1".to_string(),
263            features: vec![],
264        });
265    }
266
267    let python_bindings = if has_c {
268        Some(generate_python_bindings(&stmts, "module", &interner, &codegen_registry))
269    } else {
270        None
271    };
272
273    let (typescript_bindings, typescript_types) = if has_c {
274        let (js, dts) = generate_typescript_bindings(&stmts, "module", &interner, &codegen_registry);
275        (Some(js), Some(dts))
276    } else {
277        (None, None)
278    };
279
280    Ok(CompileOutput { rust_code, dependencies, c_header, python_bindings, typescript_types, typescript_bindings })
281}
282
283/// Extract crate dependencies from `Stmt::Require` nodes.
284///
285/// Deduplicates by crate name: same name + same version keeps one copy.
286/// Same name + different version returns a `ParseError`.
287/// Preserves declaration order (first occurrence wins).
288fn extract_dependencies(stmts: &[Stmt], interner: &Interner) -> Result<Vec<CrateDependency>, ParseError> {
289    use std::collections::HashMap;
290
291    let mut seen: HashMap<String, String> = HashMap::new(); // name → version
292    let mut deps: Vec<CrateDependency> = Vec::new();
293
294    for stmt in stmts {
295        if let Stmt::Require { crate_name, version, features, span } = stmt {
296            let name = interner.resolve(*crate_name).to_string();
297            let ver = interner.resolve(*version).to_string();
298
299            if let Some(existing_ver) = seen.get(&name) {
300                if *existing_ver != ver {
301                    return Err(ParseError {
302                        kind: crate::error::ParseErrorKind::Custom(format!(
303                            "Conflicting versions for crate \"{}\": \"{}\" and \"{}\".",
304                            name, existing_ver, ver
305                        )),
306                        span: *span,
307                    });
308                }
309                // Same name + same version: skip duplicate
310            } else {
311                seen.insert(name.clone(), ver.clone());
312                deps.push(CrateDependency {
313                    name,
314                    version: ver,
315                    features: features.iter().map(|f| interner.resolve(*f).to_string()).collect(),
316                });
317            }
318        }
319    }
320
321    Ok(deps)
322}
323
324/// Compile LOGOS source to Rust with ownership checking enabled.
325///
326/// This runs the lightweight ownership analysis pass that catches use-after-move
327/// errors with control flow awareness. The analysis is fast enough to run on
328/// every keystroke in an IDE.
329///
330/// # Arguments
331///
332/// * `source` - LOGOS source code as a string
333///
334/// # Returns
335///
336/// Generated Rust source code on success.
337///
338/// # Errors
339///
340/// Returns [`ParseError`] if:
341/// - Any error from [`compile_to_rust`] occurs
342/// - Ownership analysis detects use-after-move
343/// - Ownership analysis detects use-after-borrow violations
344///
345/// # Example
346///
347/// ```
348/// # use logicaffeine_compile::compile::compile_to_rust_checked;
349/// // This will fail ownership checking
350/// let source = "## Main\nLet x be 5.\nGive x to y.\nShow x.";
351/// let result = compile_to_rust_checked(source);
352/// assert!(result.is_err()); // "x has already been given away"
353/// ```
354///
355/// # Use Case
356///
357/// Use this function with the `--check` CLI flag for instant feedback on
358/// ownership errors before running the full Rust compilation.
359pub fn compile_to_rust_checked(source: &str) -> Result<String, ParseError> {
360    let mut interner = Interner::new();
361    let mut lexer = Lexer::new(source, &mut interner);
362    let tokens = lexer.tokenize();
363
364    // Pass 1: Discovery - scan for type definitions and policies
365    let (type_registry, policy_registry) = {
366        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
367        let result = discovery.run_full();
368        (result.types, result.policies)
369    };
370    // Clone for codegen (parser takes ownership)
371    let codegen_registry = type_registry.clone();
372    let codegen_policies = policy_registry.clone();
373
374    let mut world_state = WorldState::new();
375    let expr_arena = Arena::new();
376    let term_arena = Arena::new();
377    let np_arena = Arena::new();
378    let sym_arena = Arena::new();
379    let role_arena = Arena::new();
380    let pp_arena = Arena::new();
381    let stmt_arena: Arena<Stmt> = Arena::new();
382    let imperative_expr_arena: Arena<Expr> = Arena::new();
383    let type_expr_arena: Arena<TypeExpr> = Arena::new();
384
385    let ast_ctx = AstContext::with_types(
386        &expr_arena,
387        &term_arena,
388        &np_arena,
389        &sym_arena,
390        &role_arena,
391        &pp_arena,
392        &stmt_arena,
393        &imperative_expr_arena,
394        &type_expr_arena,
395    );
396
397    // Pass 2: Parse with type context
398    let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ast_ctx, type_registry);
399    let stmts = parser.parse_program()?;
400
401    // Pass 3: Escape analysis
402    let mut escape_checker = EscapeChecker::new(&interner);
403    escape_checker.check_program(&stmts).map_err(|e| {
404        ParseError {
405            kind: crate::error::ParseErrorKind::Custom(e.to_string()),
406            span: e.span,
407        }
408    })?;
409
410    // Pass 4: Ownership analysis
411    // Catches use-after-move errors with control flow awareness
412    let mut ownership_checker = OwnershipChecker::new(&interner);
413    ownership_checker.check_program(&stmts).map_err(|e| {
414        ParseError {
415            kind: crate::error::ParseErrorKind::Custom(e.to_string()),
416            span: e.span,
417        }
418    })?;
419
420    let rust_code = codegen_program(&stmts, &codegen_registry, &codegen_policies, &interner);
421
422    Ok(rust_code)
423}
424
425/// Compile LOGOS source to Rust with full Z3 static verification.
426///
427/// This runs the Z3-based verifier on Assert statements before code generation,
428/// proving that assertions hold for all possible inputs. This is the most
429/// thorough compilation mode, suitable for high-assurance code.
430///
431/// # Arguments
432///
433/// * `source` - LOGOS source code as a string
434///
435/// # Returns
436///
437/// Generated Rust source code on success.
438///
439/// # Errors
440///
441/// Returns [`ParseError`] if:
442/// - Any error from [`compile_to_rust`] occurs
443/// - Z3 cannot prove an Assert statement
444/// - Refinement type constraints cannot be satisfied
445/// - Termination cannot be proven for loops with `decreasing`
446///
447/// # Example
448///
449/// ```no_run
450/// # use logicaffeine_compile::compile::compile_to_rust_verified;
451/// # use logicaffeine_compile::ParseError;
452/// # fn main() -> Result<(), ParseError> {
453/// let source = r#"
454/// ## Main
455/// Let x: { it: Int | it > 0 } be 5.
456/// Assert that x > 0.
457/// "#;
458/// let rust_code = compile_to_rust_verified(source)?;
459/// # Ok(())
460/// # }
461/// ```
462///
463/// # Feature Flag
464///
465/// This function requires the `verification` feature to be enabled:
466///
467/// ```toml
468/// [dependencies]
469/// logicaffeine_compile = { version = "...", features = ["verification"] }
470/// ```
471#[cfg(feature = "verification")]
472pub fn compile_to_rust_verified(source: &str) -> Result<String, ParseError> {
473    use crate::verification::VerificationPass;
474
475    let mut interner = Interner::new();
476    let mut lexer = Lexer::new(source, &mut interner);
477    let tokens = lexer.tokenize();
478
479    // Pass 1: Discovery - scan for type definitions and policies
480    let (type_registry, policy_registry) = {
481        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
482        let result = discovery.run_full();
483        (result.types, result.policies)
484    };
485    // Clone for codegen (parser takes ownership)
486    let codegen_registry = type_registry.clone();
487    let codegen_policies = policy_registry.clone();
488
489    let mut world_state = WorldState::new();
490    let expr_arena = Arena::new();
491    let term_arena = Arena::new();
492    let np_arena = Arena::new();
493    let sym_arena = Arena::new();
494    let role_arena = Arena::new();
495    let pp_arena = Arena::new();
496    let stmt_arena: Arena<Stmt> = Arena::new();
497    let imperative_expr_arena: Arena<Expr> = Arena::new();
498    let type_expr_arena: Arena<TypeExpr> = Arena::new();
499
500    let ast_ctx = AstContext::with_types(
501        &expr_arena,
502        &term_arena,
503        &np_arena,
504        &sym_arena,
505        &role_arena,
506        &pp_arena,
507        &stmt_arena,
508        &imperative_expr_arena,
509        &type_expr_arena,
510    );
511
512    // Pass 2: Parse with type context
513    let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ast_ctx, type_registry);
514    let stmts = parser.parse_program()?;
515
516    // Pass 3: Escape analysis
517    let mut escape_checker = EscapeChecker::new(&interner);
518    escape_checker.check_program(&stmts).map_err(|e| {
519        ParseError {
520            kind: crate::error::ParseErrorKind::Custom(e.to_string()),
521            span: e.span,
522        }
523    })?;
524
525    // Pass 4: Static verification
526    let mut verifier = VerificationPass::new(&interner);
527    verifier.verify_program(&stmts).map_err(|e| {
528        ParseError {
529            kind: crate::error::ParseErrorKind::Custom(format!(
530                "Verification Failed:\n\n{}",
531                e
532            )),
533            span: crate::token::Span::default(),
534        }
535    })?;
536
537    let rust_code = codegen_program(&stmts, &codegen_registry, &codegen_policies, &interner);
538
539    Ok(rust_code)
540}
541
542/// Compile LOGOS source and write output to a directory as a Cargo project.
543///
544/// Creates a complete Cargo project structure with:
545/// - `src/main.rs` containing the generated Rust code
546/// - `Cargo.toml` with runtime dependencies
547/// - `crates/` directory with runtime crate copies
548///
549/// # Arguments
550///
551/// * `source` - LOGOS source code as a string
552/// * `output_dir` - Directory to create the Cargo project in
553///
554/// # Errors
555///
556/// Returns [`CompileError`] if:
557/// - Compilation fails (wrapped as `CompileError::Parse`)
558/// - File system operations fail (wrapped as `CompileError::Io`)
559///
560/// # Example
561///
562/// ```no_run
563/// # use logicaffeine_compile::compile::{compile_to_dir, CompileError};
564/// # use std::path::Path;
565/// # fn main() -> Result<(), CompileError> {
566/// let source = "## Main\nShow \"Hello\".";
567/// compile_to_dir(source, Path::new("/tmp/my_project"))?;
568/// // Now /tmp/my_project is a buildable Cargo project
569/// # Ok(())
570/// # }
571/// ```
572pub fn compile_to_dir(source: &str, output_dir: &Path) -> Result<(), CompileError> {
573    let output = compile_program_full(source).map_err(CompileError::Parse)?;
574
575    // Create output directory structure
576    let src_dir = output_dir.join("src");
577    fs::create_dir_all(&src_dir).map_err(|e| CompileError::Io(e.to_string()))?;
578
579    // Write main.rs (codegen already includes the use statements)
580    let main_path = src_dir.join("main.rs");
581    let mut file = fs::File::create(&main_path).map_err(|e| CompileError::Io(e.to_string()))?;
582    file.write_all(output.rust_code.as_bytes()).map_err(|e| CompileError::Io(e.to_string()))?;
583
584    // Write Cargo.toml with runtime crate dependencies
585    let mut cargo_toml = String::from(r#"[package]
586name = "logos_output"
587version = "0.1.0"
588edition = "2021"
589
590[dependencies]
591logicaffeine-data = { path = "./crates/logicaffeine_data" }
592logicaffeine-system = { path = "./crates/logicaffeine_system", features = ["full"] }
593tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
594"#);
595
596    // Append user-declared dependencies from ## Requires blocks
597    for dep in &output.dependencies {
598        if dep.features.is_empty() {
599            let _ = writeln!(cargo_toml, "{} = \"{}\"", dep.name, dep.version);
600        } else {
601            let feats = dep.features.iter()
602                .map(|f| format!("\"{}\"", f))
603                .collect::<Vec<_>>()
604                .join(", ");
605            let _ = writeln!(
606                cargo_toml,
607                "{} = {{ version = \"{}\", features = [{}] }}",
608                dep.name, dep.version, feats
609            );
610        }
611    }
612
613    let cargo_path = output_dir.join("Cargo.toml");
614    let mut file = fs::File::create(&cargo_path).map_err(|e| CompileError::Io(e.to_string()))?;
615    file.write_all(cargo_toml.as_bytes()).map_err(|e| CompileError::Io(e.to_string()))?;
616
617    // Copy runtime crates to output directory
618    copy_runtime_crates(output_dir)?;
619
620    Ok(())
621}
622
623/// Copy the runtime crates to the output directory.
624/// Copies logicaffeine_data and logicaffeine_system.
625pub fn copy_runtime_crates(output_dir: &Path) -> Result<(), CompileError> {
626    let crates_dir = output_dir.join("crates");
627    fs::create_dir_all(&crates_dir).map_err(|e| CompileError::Io(e.to_string()))?;
628
629    // Find workspace root
630    let workspace_root = find_workspace_root()?;
631
632    // Copy logicaffeine_data
633    let data_src = workspace_root.join(CRATES_DATA_PATH);
634    let data_dest = crates_dir.join("logicaffeine_data");
635    copy_dir_recursive(&data_src, &data_dest)?;
636
637    // Copy logicaffeine_system
638    let system_src = workspace_root.join(CRATES_SYSTEM_PATH);
639    let system_dest = crates_dir.join("logicaffeine_system");
640    copy_dir_recursive(&system_src, &system_dest)?;
641
642    // Also need to copy logicaffeine_base since both crates depend on it
643    let base_src = workspace_root.join("crates/logicaffeine_base");
644    let base_dest = crates_dir.join("logicaffeine_base");
645    copy_dir_recursive(&base_src, &base_dest)?;
646
647    Ok(())
648}
649
650/// Find the workspace root directory.
651fn find_workspace_root() -> Result<std::path::PathBuf, CompileError> {
652    // Try CARGO_MANIFEST_DIR first
653    if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
654        // Go up from crates/logicaffeine_compile to workspace root
655        let path = Path::new(&manifest_dir);
656        if let Some(parent) = path.parent().and_then(|p| p.parent()) {
657            if parent.join("Cargo.toml").exists() {
658                return Ok(parent.to_path_buf());
659            }
660        }
661    }
662
663    // Fallback to current directory traversal
664    let mut current = std::env::current_dir()
665        .map_err(|e| CompileError::Io(e.to_string()))?;
666
667    loop {
668        if current.join("Cargo.toml").exists() && current.join("crates").exists() {
669            return Ok(current);
670        }
671        if !current.pop() {
672            return Err(CompileError::Io("Could not find workspace root".to_string()));
673        }
674    }
675}
676
677/// Recursively copy a directory.
678/// Skips files that disappear during copy (race condition with parallel builds).
679fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), CompileError> {
680    fs::create_dir_all(dst).map_err(|e| CompileError::Io(e.to_string()))?;
681
682    for entry in fs::read_dir(src).map_err(|e| CompileError::Io(e.to_string()))? {
683        let entry = entry.map_err(|e| CompileError::Io(e.to_string()))?;
684        let src_path = entry.path();
685        let file_name = entry.file_name();
686        let dst_path = dst.join(&file_name);
687
688        // Skip target directory, build artifacts, and lock files
689        if file_name == "target"
690            || file_name == ".git"
691            || file_name == "Cargo.lock"
692            || file_name == ".DS_Store"
693        {
694            continue;
695        }
696
697        // Skip files that start with a dot (hidden files)
698        if file_name.to_string_lossy().starts_with('.') {
699            continue;
700        }
701
702        // Check if path still exists (race condition protection)
703        if !src_path.exists() {
704            continue;
705        }
706
707        if src_path.is_dir() {
708            copy_dir_recursive(&src_path, &dst_path)?;
709        } else if file_name == "Cargo.toml" {
710            // Special handling for Cargo.toml: remove [workspace] line
711            // which can interfere with nested crate dependencies
712            match fs::read_to_string(&src_path) {
713                Ok(content) => {
714                    let filtered: String = content
715                        .lines()
716                        .filter(|line| !line.trim().starts_with("[workspace]"))
717                        .collect::<Vec<_>>()
718                        .join("\n");
719                    fs::write(&dst_path, filtered)
720                        .map_err(|e| CompileError::Io(e.to_string()))?;
721                }
722                Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
723                Err(e) => return Err(CompileError::Io(e.to_string())),
724            }
725        } else {
726            match fs::copy(&src_path, &dst_path) {
727                Ok(_) => {}
728                Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
729                Err(e) => return Err(CompileError::Io(e.to_string())),
730            }
731        }
732    }
733
734    Ok(())
735}
736
737/// Compile and run a LOGOS program end-to-end.
738///
739/// This function performs the full compilation workflow:
740/// 1. Compile LOGOS to Rust via [`compile_to_dir`]
741/// 2. Run `cargo build` with JSON diagnostics
742/// 3. Translate any rustc errors to LOGOS-friendly messages
743/// 4. Run the compiled program via `cargo run`
744///
745/// # Arguments
746///
747/// * `source` - LOGOS source code as a string
748/// * `output_dir` - Directory to create the temporary Cargo project in
749///
750/// # Returns
751///
752/// The stdout output of the executed program.
753///
754/// # Errors
755///
756/// Returns [`CompileError`] if:
757/// - Compilation fails (see [`compile_to_dir`])
758/// - Rust compilation fails (`CompileError::Build` or `CompileError::Ownership`)
759/// - The program crashes at runtime (`CompileError::Runtime`)
760///
761/// # Diagnostic Translation
762///
763/// When rustc reports errors (e.g., E0382 for use-after-move), this function
764/// uses the [`diagnostic`](crate::diagnostic) module to translate them into
765/// LOGOS-friendly Socratic error messages.
766///
767/// # Example
768///
769/// ```no_run
770/// # use logicaffeine_compile::compile::{compile_and_run, CompileError};
771/// # use std::path::Path;
772/// # fn main() -> Result<(), CompileError> {
773/// let source = "## Main\nShow \"Hello, World!\".";
774/// let output = compile_and_run(source, Path::new("/tmp/run"))?;
775/// assert_eq!(output.trim(), "Hello, World!");
776/// # Ok(())
777/// # }
778/// ```
779pub fn compile_and_run(source: &str, output_dir: &Path) -> Result<String, CompileError> {
780    compile_to_dir(source, output_dir)?;
781
782    // Run cargo build with JSON message format for structured error parsing
783    let build_output = Command::new("cargo")
784        .arg("build")
785        .arg("--message-format=json")
786        .current_dir(output_dir)
787        .output()
788        .map_err(|e| CompileError::Io(e.to_string()))?;
789
790    if !build_output.status.success() {
791        let stderr = String::from_utf8_lossy(&build_output.stderr);
792        let stdout = String::from_utf8_lossy(&build_output.stdout);
793
794        // Try to parse JSON diagnostics and translate them
795        let diagnostics = parse_rustc_json(&stdout);
796
797        if !diagnostics.is_empty() {
798            // Create a basic source map with the LOGOS source
799            let source_map = SourceMap::new(source.to_string());
800            let interner = Interner::new();
801
802            if let Some(logos_error) = translate_diagnostics(&diagnostics, &source_map, &interner) {
803                return Err(CompileError::Ownership(logos_error));
804            }
805        }
806
807        // Fallback to raw error if translation fails
808        return Err(CompileError::Build(stderr.to_string()));
809    }
810
811    // Run the compiled program
812    let run_output = Command::new("cargo")
813        .arg("run")
814        .arg("--quiet")
815        .current_dir(output_dir)
816        .output()
817        .map_err(|e| CompileError::Io(e.to_string()))?;
818
819    if !run_output.status.success() {
820        let stderr = String::from_utf8_lossy(&run_output.stderr);
821        return Err(CompileError::Runtime(stderr.to_string()));
822    }
823
824    let stdout = String::from_utf8_lossy(&run_output.stdout);
825    Ok(stdout.to_string())
826}
827
828/// Compile a LOGOS source file.
829/// For single-file compilation without dependencies.
830pub fn compile_file(path: &Path) -> Result<String, CompileError> {
831    let source = fs::read_to_string(path).map_err(|e| CompileError::Io(e.to_string()))?;
832    compile_to_rust(&source).map_err(CompileError::Parse)
833}
834
835/// Compile a multi-file LOGOS project with dependency resolution.
836///
837/// This function:
838/// 1. Reads the entry file
839/// 2. Scans for dependencies in the abstract (Markdown links)
840/// 3. Recursively loads and discovers types from dependencies
841/// 4. Compiles with the combined type registry
842///
843/// # Arguments
844/// * `entry_file` - The main entry file to compile (root is derived from parent directory)
845///
846/// # Example
847/// ```no_run
848/// # use logicaffeine_compile::compile::compile_project;
849/// # use std::path::Path;
850/// let result = compile_project(Path::new("/project/main.md"));
851/// ```
852pub fn compile_project(entry_file: &Path) -> Result<CompileOutput, CompileError> {
853    use crate::loader::Loader;
854    use crate::analysis::discover_with_imports;
855
856    let root_path = entry_file.parent().unwrap_or(Path::new(".")).to_path_buf();
857    let mut loader = Loader::new(root_path);
858    let mut interner = Interner::new();
859
860    // Read the entry file
861    let source = fs::read_to_string(entry_file)
862        .map_err(|e| CompileError::Io(format!("Failed to read entry file: {}", e)))?;
863
864    // Discover types from entry file and all imports
865    let type_registry = discover_with_imports(entry_file, &source, &mut loader, &mut interner)
866        .map_err(|e| CompileError::Io(e))?;
867
868    // Now compile with the discovered types
869    compile_to_rust_with_registry_full(&source, type_registry, &mut interner)
870        .map_err(CompileError::Parse)
871}
872
873/// Compile LOGOS source with a pre-populated type registry, returning full output.
874/// Returns both generated Rust code and extracted dependencies.
875fn compile_to_rust_with_registry_full(
876    source: &str,
877    type_registry: crate::analysis::TypeRegistry,
878    interner: &mut Interner,
879) -> Result<CompileOutput, ParseError> {
880    let mut lexer = Lexer::new(source, interner);
881    let tokens = lexer.tokenize();
882
883    // Discovery pass for policies (types already discovered)
884    let policy_registry = {
885        let mut discovery = DiscoveryPass::new(&tokens, interner);
886        discovery.run_full().policies
887    };
888
889    let codegen_registry = type_registry.clone();
890    let codegen_policies = policy_registry.clone();
891
892    let mut world_state = WorldState::new();
893    let expr_arena = Arena::new();
894    let term_arena = Arena::new();
895    let np_arena = Arena::new();
896    let sym_arena = Arena::new();
897    let role_arena = Arena::new();
898    let pp_arena = Arena::new();
899    let stmt_arena: Arena<Stmt> = Arena::new();
900    let imperative_expr_arena: Arena<Expr> = Arena::new();
901    let type_expr_arena: Arena<TypeExpr> = Arena::new();
902
903    let ast_ctx = AstContext::with_types(
904        &expr_arena,
905        &term_arena,
906        &np_arena,
907        &sym_arena,
908        &role_arena,
909        &pp_arena,
910        &stmt_arena,
911        &imperative_expr_arena,
912        &type_expr_arena,
913    );
914
915    let mut parser = Parser::new(tokens, &mut world_state, interner, ast_ctx, type_registry);
916    let stmts = parser.parse_program()?;
917
918    // Extract dependencies before escape analysis
919    let mut dependencies = extract_dependencies(&stmts, interner)?;
920
921    // FFI: Auto-inject wasm-bindgen dependency if any function is exported for WASM
922    let needs_wasm_bindgen = stmts.iter().any(|stmt| {
923        if let Stmt::FunctionDef { is_exported: true, export_target: Some(target), .. } = stmt {
924            interner.resolve(*target).eq_ignore_ascii_case("wasm")
925        } else {
926            false
927        }
928    });
929    if needs_wasm_bindgen && !dependencies.iter().any(|d| d.name == "wasm-bindgen") {
930        dependencies.push(CrateDependency {
931            name: "wasm-bindgen".to_string(),
932            version: "0.2".to_string(),
933            features: vec![],
934        });
935    }
936
937    let mut escape_checker = EscapeChecker::new(interner);
938    escape_checker.check_program(&stmts).map_err(|e| {
939        ParseError {
940            kind: crate::error::ParseErrorKind::Custom(e.to_string()),
941            span: e.span,
942        }
943    })?;
944
945    let rust_code = codegen_program(&stmts, &codegen_registry, &codegen_policies, interner);
946
947    // Universal ABI: Generate C header + bindings if any C exports exist
948    let has_c = stmts.iter().any(|stmt| {
949        if let Stmt::FunctionDef { is_exported: true, export_target, .. } = stmt {
950            match export_target {
951                None => true,
952                Some(t) => interner.resolve(*t).eq_ignore_ascii_case("c"),
953            }
954        } else {
955            false
956        }
957    });
958
959    let c_header = if has_c {
960        Some(generate_c_header(&stmts, "module", interner, &codegen_registry))
961    } else {
962        None
963    };
964
965    if has_c && !dependencies.iter().any(|d| d.name == "serde_json") {
966        dependencies.push(CrateDependency {
967            name: "serde_json".to_string(),
968            version: "1".to_string(),
969            features: vec![],
970        });
971    }
972
973    let python_bindings = if has_c {
974        Some(generate_python_bindings(&stmts, "module", interner, &codegen_registry))
975    } else {
976        None
977    };
978
979    let (typescript_bindings, typescript_types) = if has_c {
980        let (js, dts) = generate_typescript_bindings(&stmts, "module", interner, &codegen_registry);
981        (Some(js), Some(dts))
982    } else {
983        (None, None)
984    };
985
986    Ok(CompileOutput { rust_code, dependencies, c_header, python_bindings, typescript_types, typescript_bindings })
987}
988
989/// Errors that can occur during the LOGOS compilation pipeline.
990///
991/// This enum represents the different stages where compilation can fail,
992/// from parsing through to runtime execution.
993///
994/// # Error Hierarchy
995///
996/// ```text
997/// CompileError
998/// ├── Parse      ← Lexing, parsing, or static analysis
999/// ├── Io         ← File system operations
1000/// ├── Build      ← Rust compilation (cargo build)
1001/// ├── Ownership  ← Translated borrow checker errors
1002/// └── Runtime    ← Program execution failure
1003/// ```
1004///
1005/// # Error Translation
1006///
1007/// The `Ownership` variant contains LOGOS-friendly error messages translated
1008/// from rustc's borrow checker errors (E0382, E0505, E0597) using the
1009/// [`diagnostic`](crate::diagnostic) module.
1010#[derive(Debug)]
1011pub enum CompileError {
1012    /// Parsing or static analysis failed.
1013    ///
1014    /// This includes lexer errors, syntax errors, escape analysis failures,
1015    /// ownership analysis failures, and Z3 verification failures.
1016    Parse(ParseError),
1017
1018    /// File system operation failed.
1019    ///
1020    /// Typically occurs when reading source files or writing output.
1021    Io(String),
1022
1023    /// Rust compilation failed (`cargo build`).
1024    ///
1025    /// Contains the raw stderr output from rustc when diagnostic translation
1026    /// was not possible.
1027    Build(String),
1028
1029    /// Runtime execution failed.
1030    ///
1031    /// Contains stderr output from the executed program.
1032    Runtime(String),
1033
1034    /// Translated ownership/borrow checker error with LOGOS-friendly message.
1035    ///
1036    /// This variant is used when rustc reports errors like E0382 (use after move)
1037    /// and we can translate them into natural language error messages that
1038    /// reference the original LOGOS source.
1039    Ownership(LogosError),
1040}
1041
1042impl std::fmt::Display for CompileError {
1043    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1044        match self {
1045            CompileError::Parse(e) => write!(f, "Parse error: {:?}", e),
1046            CompileError::Io(e) => write!(f, "IO error: {}", e),
1047            CompileError::Build(e) => write!(f, "Build error: {}", e),
1048            CompileError::Runtime(e) => write!(f, "Runtime error: {}", e),
1049            CompileError::Ownership(e) => write!(f, "{}", e),
1050        }
1051    }
1052}
1053
1054impl std::error::Error for CompileError {}
1055
1056#[cfg(test)]
1057mod tests {
1058    use super::*;
1059
1060    #[test]
1061    fn test_compile_let_statement() {
1062        let source = "## Main\nLet x be 5.";
1063        let result = compile_to_rust(source);
1064        assert!(result.is_ok(), "Should compile: {:?}", result);
1065        let rust = result.unwrap();
1066        assert!(rust.contains("fn main()"));
1067        assert!(rust.contains("let x = 5;"));
1068    }
1069
1070    #[test]
1071    fn test_compile_return_statement() {
1072        let source = "## Main\nReturn 42.";
1073        let result = compile_to_rust(source);
1074        assert!(result.is_ok(), "Should compile: {:?}", result);
1075        let rust = result.unwrap();
1076        assert!(rust.contains("return 42;"));
1077    }
1078}