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