1use std::fs;
74use std::io::Write;
75use std::path::Path;
76use std::process::Command;
77
78const 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#[derive(Debug, Clone)]
99pub struct CrateDependency {
100 pub name: String,
101 pub version: String,
102 pub features: Vec<String>,
103}
104
105#[derive(Debug)]
107pub struct CompileOutput {
108 pub rust_code: String,
109 pub dependencies: Vec<CrateDependency>,
110 pub c_header: Option<String>,
112 pub python_bindings: Option<String>,
114 pub typescript_types: Option<String>,
116 pub typescript_bindings: Option<String>,
118}
119
120pub 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
149pub fn compile_to_rust(source: &str) -> Result<String, ParseError> {
182 compile_program_full(source).map(|o| o.rust_code)
183}
184
185pub 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 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 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 let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ast_ctx, type_registry);
229 let stmts = parser.parse_program()?;
232
233 let stmts = crate::optimize::optimize_program(stmts, &imperative_expr_arena, &stmt_arena, &mut interner);
235
236 let mut dependencies = extract_dependencies(&stmts, &interner)?;
238
239 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 let mut escape_checker = EscapeChecker::new(&interner);
258 escape_checker.check_program(&stmts).map_err(|e| {
259 ParseError {
262 kind: crate::error::ParseErrorKind::Custom(e.to_string()),
263 span: e.span,
264 }
265 })?;
266
267 let rust_code = codegen_program(&stmts, &codegen_registry, &codegen_policies, &interner);
271
272 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 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
315fn 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(); 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 } 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
356pub 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 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 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 let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ast_ctx, type_registry);
431 let stmts = parser.parse_program()?;
432
433 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 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#[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 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 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 let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ast_ctx, type_registry);
546 let stmts = parser.parse_program()?;
547
548 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 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
574pub fn compile_to_dir(source: &str, output_dir: &Path) -> Result<(), CompileError> {
605 let output = compile_program_full(source).map_err(CompileError::Parse)?;
606
607 let src_dir = output_dir.join("src");
609 fs::create_dir_all(&src_dir).map_err(|e| CompileError::Io(e.to_string()))?;
610
611 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 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 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(output_dir)?;
653
654 Ok(())
655}
656
657pub 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 let workspace_root = find_workspace_root()?;
665
666 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 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 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
687fn 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 continue;
713 } else if trimmed.contains(".workspace = true") {
714 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
728fn find_workspace_root() -> Result<std::path::PathBuf, CompileError> {
730 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 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 if let Ok(exe) = std::env::current_exe() {
751 if let Some(dir) = exe.parent() {
752 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 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
781fn 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 if file_name == "target"
794 || file_name == ".git"
795 || file_name == "Cargo.lock"
796 || file_name == ".DS_Store"
797 {
798 continue;
799 }
800
801 if file_name.to_string_lossy().starts_with('.') {
803 continue;
804 }
805
806 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 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
841pub fn compile_and_run(source: &str, output_dir: &Path) -> Result<String, CompileError> {
884 compile_to_dir(source, output_dir)?;
885
886 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 let diagnostics = parse_rustc_json(&stdout);
900
901 if !diagnostics.is_empty() {
902 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 return Err(CompileError::Build(stderr.to_string()));
913 }
914
915 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
932pub 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
939pub 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 let source = fs::read_to_string(entry_file)
966 .map_err(|e| CompileError::Io(format!("Failed to read entry file: {}", e)))?;
967
968 let type_registry = discover_with_imports(entry_file, &source, &mut loader, &mut interner)
970 .map_err(|e| CompileError::Io(e))?;
971
972 compile_to_rust_with_registry_full(&source, type_registry, &mut interner)
974 .map_err(CompileError::Parse)
975}
976
977fn 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 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 let mut dependencies = extract_dependencies(&stmts, interner)?;
1024
1025 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 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#[derive(Debug)]
1115pub enum CompileError {
1116 Parse(ParseError),
1121
1122 Io(String),
1126
1127 Build(String),
1132
1133 Runtime(String),
1137
1138 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}