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