1use std::fmt::Write;
12
13pub mod hir;
15pub mod type_map;
17
18mod block;
19mod contract;
20mod decl;
21mod expr;
22pub mod features;
24pub mod metadata;
26mod service;
27mod types_gen;
28
29pub use contract::{collect_contract_params, extract_input_params};
30pub use expr::expr_to_rust_static;
31pub use types_gen::{map_type_token, map_type_tokens};
32
33use block::*;
34use contract::*;
35use decl::*;
36use expr::*;
37use service::*;
38use types_gen::*;
39
40use assura_ast::{
41 BinOp, BindDecl, BlockKind, Clause, ClauseKind, CodecRegistryDecl, ContractDecl, Decl,
42 DeclVisitor, EnumDef, Expr, ExternDecl, FnDef, Literal, MagicPattern, ServiceDecl, ServiceItem,
43 SpExpr, Spanned, TypeBody, TypeDef, UnaryOp,
44};
45use assura_types::TypedFile;
46
47const GENERATED_ALLOW_ATTRS: &str = "#![allow(dead_code, unused_variables, unused_imports, unused_parens, non_camel_case_types, unreachable_code)]\n\n";
49
50const GENERATED_HEADER: &str = "// Generated by the Assura compiler.\n// Do not edit manually.\n\n";
52
53#[cfg(test)]
54mod codegen_tests;
55
56#[derive(Debug, Clone)]
62pub struct GeneratedProject {
63 pub cargo_toml: String,
65 pub files: Vec<(String, String)>,
68 pub metadata: Option<metadata::ProjectMetadata>,
71}
72
73#[derive(Debug, Clone, PartialEq, Default)]
79pub enum CodegenBackend {
80 #[default]
82 Rustc,
83 Cranelift,
85}
86
87#[derive(Debug, Clone, PartialEq, Default)]
89pub enum CompileTarget {
90 #[default]
92 Native,
93 Wasm,
95}
96
97impl CompileTarget {
98 pub fn from_str_loose(s: &str) -> Option<Self> {
100 match s.to_lowercase().as_str() {
101 "native" => Some(Self::Native),
102 "wasm" | "wasm32-wasi" | "wasm32-wasip1" => Some(Self::Wasm),
103 _ => None,
104 }
105 }
106
107 pub fn rust_target(&self) -> Option<&'static str> {
109 match self {
110 Self::Native => None, Self::Wasm => Some("wasm32-wasip1"),
112 }
113 }
114}
115
116#[derive(Debug, Clone)]
118pub struct BackendConfig {
119 pub backend: CodegenBackend,
120 pub opt_level: u8,
121 pub debug_info: bool,
122 pub target: CompileTarget,
123 pub ir_bodies: std::collections::HashMap<String, String>,
128 pub runtime_checks: bool,
131}
132
133impl Default for BackendConfig {
134 fn default() -> Self {
135 Self {
136 backend: CodegenBackend::Rustc,
137 opt_level: 2,
138 debug_info: false,
139 target: CompileTarget::Native,
140 ir_bodies: std::collections::HashMap::new(),
141 runtime_checks: false,
142 }
143 }
144}
145
146impl BackendConfig {
147 pub fn with_ir_bodies(ir_bodies: std::collections::HashMap<String, String>) -> Self {
149 Self {
150 ir_bodies,
151 ..Self::default()
152 }
153 }
154}
155
156struct TypeCollectVisitor<'a> {
163 defined_types: &'a mut std::collections::HashSet<String>,
164 feature_max_consts: &'a mut Vec<(String, String)>,
165 referenced_types: &'a mut std::collections::HashSet<String>,
166}
167
168impl DeclVisitor for TypeCollectVisitor<'_> {
169 fn visit_type_def(&mut self, t: &TypeDef) {
170 self.defined_types.insert(t.name.clone());
171 if let TypeBody::Struct(fields) = &t.body {
172 for f in fields {
173 collect_type_refs_from_type_expr(f.ty.as_ref(), self.referenced_types);
174 }
175 }
176 }
177 fn visit_enum_def(&mut self, e: &EnumDef) {
178 self.defined_types.insert(e.name.clone());
179 }
180 fn visit_block(
181 &mut self,
182 kind: &BlockKind,
183 name: &str,
184 value: &Option<Vec<String>>,
185 _body: &[Clause],
186 ) {
187 if *kind == BlockKind::FeatureMax {
188 let ty = value
190 .as_ref()
191 .and_then(|v| {
192 v.iter()
193 .take_while(|t| t.as_str() != "=")
194 .find(|t| t.chars().next().is_some_and(|c| c.is_uppercase()))
195 })
196 .map(|t| map_type_token(t).to_string())
197 .unwrap_or_else(|| "u64".to_string());
198 self.feature_max_consts.push((name.to_string(), ty));
199 }
200 }
201 fn visit_fn_def(&mut self, f: &FnDef) {
202 collect_type_refs_from_type_expr(f.return_ty.as_ref(), self.referenced_types);
203 for p in &f.params {
204 collect_type_refs_from_type_expr(p.ty.as_ref(), self.referenced_types);
205 }
206 }
207 fn visit_extern(&mut self, ex: &ExternDecl) {
208 collect_type_refs_from_type_expr(ex.return_ty.as_ref(), self.referenced_types);
209 for p in &ex.params {
210 collect_type_refs_from_type_expr(p.ty.as_ref(), self.referenced_types);
211 }
212 }
213 fn visit_contract(&mut self, c: &ContractDecl) {
214 for clause in &c.clauses {
215 collect_type_refs_from_expr(&clause.body, self.referenced_types);
216 }
217 }
218 fn visit_service(&mut self, s: &ServiceDecl) {
219 for item in &s.items {
220 match item {
221 ServiceItem::TypeDef(t) => {
222 self.defined_types.insert(t.name.clone());
223 }
224 ServiceItem::EnumDef(e) => {
225 self.defined_types.insert(e.name.clone());
226 }
227 ServiceItem::Operation { clauses, .. } | ServiceItem::Query { clauses, .. } => {
228 for clause in clauses {
229 collect_type_refs_from_expr(&clause.body, self.referenced_types);
230 }
231 }
232 ServiceItem::States(_) | ServiceItem::Invariant(_) | ServiceItem::Other { .. } => {}
233 }
234 }
235 }
236 fn visit_bind(&mut self, b: &BindDecl) {
237 collect_type_refs_from_type_expr(b.return_ty.as_ref(), self.referenced_types);
238 for p in &b.params {
239 collect_type_refs_from_type_expr(p.ty.as_ref(), self.referenced_types);
240 }
241 }
242 }
244
245fn collect_type_names(
250 decls: &[Spanned<Decl>],
251) -> (
252 std::collections::HashSet<String>,
253 Vec<(String, String)>,
254 std::collections::HashSet<String>,
255) {
256 let mut defined_types = std::collections::HashSet::new();
257 let mut feature_max_consts: Vec<(String, String)> = Vec::new();
258 let mut referenced_types = std::collections::HashSet::new();
259
260 let mut visitor = TypeCollectVisitor {
261 defined_types: &mut defined_types,
262 feature_max_consts: &mut feature_max_consts,
263 referenced_types: &mut referenced_types,
264 };
265 assura_ast::walk_decls(&mut visitor, decls);
266
267 for builtin in &[
269 "Int", "Nat", "Float", "Bool", "String", "Bytes", "Unit", "Never", "U8", "U16", "U32",
270 "U64", "I8", "I16", "I32", "I64", "F32", "F64", "List", "Vec", "Map", "Set", "Option",
271 "Result", "Sequence", "i64", "u64", "f64", "bool", "u8", "u16", "u32", "i8", "i16", "i32",
272 "f32", "f64",
273 ] {
274 defined_types.insert(builtin.to_string());
275 }
276
277 (defined_types, feature_max_consts, referenced_types)
278}
279
280struct TypeExprWalkVisitor<'a, F> {
285 visit: F,
286 include_typedef_fields: bool,
287 _marker: std::marker::PhantomData<&'a ()>,
288}
289
290impl<F: FnMut(Option<&assura_ast::TypeExpr>)> DeclVisitor for TypeExprWalkVisitor<'_, F> {
291 fn visit_fn_def(&mut self, f: &FnDef) {
292 (self.visit)(f.return_ty.as_ref());
293 for p in &f.params {
294 (self.visit)(p.ty.as_ref());
295 }
296 }
297 fn visit_extern(&mut self, ex: &ExternDecl) {
298 (self.visit)(ex.return_ty.as_ref());
299 for p in &ex.params {
300 (self.visit)(p.ty.as_ref());
301 }
302 }
303 fn visit_bind(&mut self, b: &BindDecl) {
304 (self.visit)(b.return_ty.as_ref());
305 for p in &b.params {
306 (self.visit)(p.ty.as_ref());
307 }
308 }
309 fn visit_type_def(&mut self, t: &TypeDef) {
310 if self.include_typedef_fields
311 && let TypeBody::Struct(fields) = &t.body
312 {
313 for f in fields {
314 (self.visit)(f.ty.as_ref());
315 }
316 }
317 }
318 }
320
321fn detect_feature_max_as_type_direct(
324 decls: &[Spanned<Decl>],
325 feature_max_consts: &[(String, String)],
326) -> std::collections::HashSet<String> {
327 let fm_set: std::collections::HashSet<&str> =
328 feature_max_consts.iter().map(|(n, _)| n.as_str()).collect();
329 let mut result = std::collections::HashSet::new();
330 let mut visitor = TypeExprWalkVisitor {
331 visit: |te: Option<&assura_ast::TypeExpr>| {
332 detect_feature_max_as_type_from_type_expr(te, &fm_set, &mut result);
333 },
334 include_typedef_fields: false,
335 _marker: std::marker::PhantomData,
336 };
337 assura_ast::walk_decls(&mut visitor, decls);
338 result
339}
340
341fn detect_generic_arities_direct(
346 decls: &[Spanned<Decl>],
347) -> (
348 std::collections::HashMap<String, Vec<GenericParamKind>>,
349 std::collections::HashSet<String>,
350) {
351 let mut type_generic_params: std::collections::HashMap<String, Vec<GenericParamKind>> =
352 std::collections::HashMap::new();
353 let mut const_generic_names = std::collections::HashSet::new();
354 let mut visitor = TypeExprWalkVisitor {
355 visit: |te: Option<&assura_ast::TypeExpr>| {
356 detect_generic_arity_from_type_expr(
357 te,
358 &mut type_generic_params,
359 &mut const_generic_names,
360 );
361 },
362 include_typedef_fields: true,
363 _marker: std::marker::PhantomData,
364 };
365 assura_ast::walk_decls(&mut visitor, decls);
366 (type_generic_params, const_generic_names)
367}
368
369fn detect_const_type_stubs_direct(
372 decls: &[Spanned<Decl>],
373 const_generic_names: &std::collections::HashSet<String>,
374 feature_max_consts: &[(String, String)],
375 defined_types: &std::collections::HashSet<String>,
376) -> Vec<String> {
377 let mut all_const_as_types = const_generic_names.clone();
378 let fm_set: std::collections::HashSet<&str> =
379 feature_max_consts.iter().map(|(n, _)| n.as_str()).collect();
380 let mut extra = std::collections::HashSet::new();
381 let mut visitor = TypeExprWalkVisitor {
382 visit: |te: Option<&assura_ast::TypeExpr>| {
383 detect_feature_max_as_type_from_type_expr(te, &fm_set, &mut extra);
384 },
385 include_typedef_fields: false,
386 _marker: std::marker::PhantomData,
387 };
388 assura_ast::walk_decls(&mut visitor, decls);
389 all_const_as_types.extend(extra);
390
391 let mut const_as_types: Vec<String> = all_const_as_types
392 .iter()
393 .filter(|n| !defined_types.contains(*n))
394 .cloned()
395 .collect();
396 const_as_types.sort();
397 const_as_types
398}
399
400fn generate_undefined_type_stubs(
402 referenced_types: &std::collections::HashSet<String>,
403 defined_types: &std::collections::HashSet<String>,
404 feature_max_consts: &[(String, String)],
405 const_as_types: &[String],
406 type_generic_params: &std::collections::HashMap<String, Vec<GenericParamKind>>,
407 code: &mut String,
408) {
409 let feature_max_set: std::collections::HashSet<String> =
410 feature_max_consts.iter().map(|(n, _)| n.clone()).collect();
411 let mut undefined: Vec<String> = referenced_types
412 .difference(defined_types)
413 .filter(|t| {
414 !t.is_empty()
416 && t.chars().next().is_some_and(|c| c.is_uppercase())
417 && t.chars().all(|c| c.is_alphanumeric() || c == '_')
418 && !feature_max_consts.iter().any(|(n, _)| n == *t)
420 && !const_as_types.iter().any(|s| s == *t)
422 && !feature_max_set.contains(*t)
424 })
425 .cloned()
426 .collect();
427 undefined.sort();
428 if !undefined.is_empty() {
429 code.push_str("// Placeholder types for types used but not defined in this file.\n");
430 code.push_str("// Replace with real definitions when implementations are provided.\n");
431 for name in &undefined {
432 let arity = type_generic_params.get(name).map_or(0, |v| v.len());
433 if arity > 0 {
434 let params: Vec<String> = (0..arity).map(|i| format!("T{i}")).collect();
435 let phantoms: Vec<String> = params
436 .iter()
437 .map(|p| format!("std::marker::PhantomData<{p}>"))
438 .collect();
439 let _ = writeln!(
440 code,
441 "#[derive(Debug, Clone, PartialEq)]\npub struct {name}<{}>({});",
442 params.join(", "),
443 phantoms.join(", ")
444 );
445 } else {
446 let _ = writeln!(
447 code,
448 "#[derive(Debug, Clone, PartialEq)]\npub struct {name};"
449 );
450 }
451 }
452 code.push('\n');
453 }
454}
455
456struct CollectModuleNames {
458 contract_names: Vec<String>,
459 service_names: Vec<String>,
460}
461
462impl DeclVisitor for CollectModuleNames {
463 fn visit_contract(&mut self, c: &ContractDecl) {
464 self.contract_names.push(c.name.clone());
465 }
466 fn visit_service(&mut self, s: &ServiceDecl) {
467 self.service_names.push(s.name.clone());
468 }
469}
470
471struct CodeGenVisitor<'a> {
477 code: &'a mut String,
478 include_contracts_services: bool,
479 ir_bodies: Option<&'a std::collections::HashMap<String, String>>,
480 runtime_checks: bool,
481}
482
483impl DeclVisitor for CodeGenVisitor<'_> {
484 fn visit_type_def(&mut self, t: &TypeDef) {
485 generate_type_def(t, self.code);
486 }
487 fn visit_enum_def(&mut self, e: &EnumDef) {
488 generate_enum_def(e, self.code);
489 }
490 fn visit_extern(&mut self, ex: &ExternDecl) {
491 generate_extern(ex, self.code);
492 }
493 fn visit_bind(&mut self, b: &BindDecl) {
494 generate_bind(b, self.code);
495 }
496 fn visit_fn_def(&mut self, f: &FnDef) {
497 if !f.is_ghost && !f.is_lemma {
498 generate_fn_def(f, self.code, self.ir_bodies);
499 }
500 }
501 fn visit_block(
502 &mut self,
503 kind: &BlockKind,
504 name: &str,
505 _value: &Option<Vec<String>>,
506 body: &[Clause],
507 ) {
508 if *kind != BlockKind::FeatureMax {
509 generate_block(kind, name, body, self.code);
510 }
511 }
512 fn visit_codec_registry(&mut self, cr: &CodecRegistryDecl) {
513 generate_codec_registry(cr, self.code);
514 }
515 fn visit_contract(&mut self, c: &ContractDecl) {
516 if self.include_contracts_services {
517 if self.runtime_checks {
518 generate_contract_runtime(c, self.code, self.ir_bodies);
519 } else {
520 generate_contract(c, self.code, self.ir_bodies);
521 }
522 }
523 }
524 fn visit_service(&mut self, s: &ServiceDecl) {
525 if self.include_contracts_services {
526 generate_service(s, self.code, self.ir_bodies);
527 }
528 }
529 }
531
532struct ModDeclVisitor<'a> {
535 code: &'a mut String,
536}
537
538impl DeclVisitor for ModDeclVisitor<'_> {
539 fn visit_contract(&mut self, c: &ContractDecl) {
540 let mod_name = c.name.to_lowercase();
541 self.code.push_str("pub mod contract_");
542 self.code.push_str(&mod_name);
543 self.code.push_str(";\n");
544 }
545 fn visit_service(&mut self, s: &ServiceDecl) {
546 let mod_name = s.name.to_lowercase();
547 self.code.push_str("pub mod ");
548 self.code.push_str(&mod_name);
549 self.code.push_str(";\n");
550 }
551 }
553
554pub fn codegen_with_config(typed: &TypedFile, config: &BackendConfig) -> GeneratedProject {
564 let source = &typed.resolved.source;
565
566 let project_name = source
567 .project
568 .as_ref()
569 .map(|p| p.name.clone())
570 .unwrap_or_else(|| "generated".to_string());
571
572 let crate_name: String = project_name
573 .chars()
574 .map(|c| {
575 if c.is_alphanumeric() || c == '_' || c == '-' {
576 c
577 } else {
578 '_'
579 }
580 })
581 .collect();
582
583 let has_proptest = source_has_testable_contracts(source);
584 let has_errors = source_has_error_types(source);
585 let cargo_toml = generate_cargo_toml_impl(&crate_name, config, has_proptest, has_errors);
586
587 let mut code = String::new();
588
589 let (defined_types, feature_max_consts, referenced_types) = collect_type_names(&source.decls);
591
592 let feature_max_used_as_type =
594 detect_feature_max_as_type_direct(&source.decls, &feature_max_consts);
595 for (name, ty) in &feature_max_consts {
596 if feature_max_used_as_type.contains(name) {
597 let value = find_feature_max_value(source, name);
600 let _ = writeln!(code, "pub const {name}_VALUE: {ty} = {value};");
601 } else {
602 let value = find_feature_max_value(source, name);
603 let _ = writeln!(code, "pub const {name}: {ty} = {value};");
604 }
605 }
606 if !feature_max_consts.is_empty() {
607 code.push('\n');
608 }
609
610 let (type_generic_params, const_generic_names) = detect_generic_arities_direct(&source.decls);
612
613 let const_as_types = detect_const_type_stubs_direct(
616 &source.decls,
617 &const_generic_names,
618 &feature_max_consts,
619 &defined_types,
620 );
621 for name in &const_as_types {
622 let _ = writeln!(
625 code,
626 "#[derive(Debug, Clone, PartialEq)]\npub struct {name}; // size param from another module"
627 );
628 }
629 if !const_as_types.is_empty() {
630 code.push('\n');
631 }
632
633 generate_undefined_type_stubs(
635 &referenced_types,
636 &defined_types,
637 &feature_max_consts,
638 &const_as_types,
639 &type_generic_params,
640 &mut code,
641 );
642
643 let mut collector = CollectModuleNames {
645 contract_names: Vec::new(),
646 service_names: Vec::new(),
647 };
648 assura_ast::walk_decls(&mut collector, &source.decls);
649 let contract_names = collector.contract_names;
650 let service_names = collector.service_names;
651 let total_modules = contract_names.len() + service_names.len();
652 let use_multi_file = total_modules >= 2;
653
654 let mut project = if use_multi_file {
655 let mut files: Vec<(String, String)> = Vec::new();
660
661 let mut shared = String::new();
663 shared.push_str(GENERATED_ALLOW_ATTRS);
664 shared.push_str(&code);
667
668 let ir_ref = if config.ir_bodies.is_empty() {
671 None
672 } else {
673 Some(&config.ir_bodies)
674 };
675 let mut codegen_visitor = CodeGenVisitor {
676 code: &mut shared,
677 include_contracts_services: false,
678 ir_bodies: ir_ref,
679 runtime_checks: config.runtime_checks,
680 };
681 assura_ast::walk_decls(&mut codegen_visitor, &source.decls);
682
683 let mut mod_visitor = ModDeclVisitor { code: &mut shared };
685 assura_ast::walk_decls(&mut mod_visitor, &source.decls);
686
687 let formatted_shared = format_rust(&shared);
688 let lib_rs = format!("{GENERATED_HEADER}{formatted_shared}");
689 files.push(("src/lib.rs".to_string(), lib_rs));
690
691 for decl in &source.decls {
693 if let Decl::Contract(c) = &decl.node {
694 let mod_name = c.name.to_lowercase();
695 let mut mod_code = String::new();
696 mod_code.push_str(GENERATED_ALLOW_ATTRS);
697 mod_code.push_str("use super::*;\n\n");
698 generate_contract_contents_opts(c, &mut mod_code, ir_ref, config.runtime_checks);
701 generate_proptest_for_contract_contents(c, &mut mod_code);
703 let formatted = format_rust(&mod_code);
704 let content = format!("{GENERATED_HEADER}{formatted}");
705 files.push((format!("src/contract_{mod_name}.rs"), content));
706 }
707 }
708
709 for decl in &source.decls {
711 if let Decl::Service(s) = &decl.node {
712 let mod_name = s.name.to_lowercase();
713 let mut mod_code = String::new();
714 mod_code.push_str(GENERATED_ALLOW_ATTRS);
715 mod_code.push_str("use super::*;\n\n");
716 generate_service_contents(s, &mut mod_code, ir_ref);
717 let formatted = format_rust(&mod_code);
718 let content = format!("{GENERATED_HEADER}{formatted}");
719 files.push((format!("src/{mod_name}.rs"), content));
720 }
721 }
722
723 GeneratedProject {
724 cargo_toml: cargo_toml.clone(),
725 files,
726 metadata: None,
727 }
728 } else {
729 let mut all_code = String::new();
733 all_code.push_str(GENERATED_ALLOW_ATTRS);
734 all_code.push_str(&code);
735
736 let ir_ref = if config.ir_bodies.is_empty() {
738 None
739 } else {
740 Some(&config.ir_bodies)
741 };
742 let mut codegen_visitor = CodeGenVisitor {
743 code: &mut all_code,
744 include_contracts_services: true,
745 ir_bodies: ir_ref,
746 runtime_checks: config.runtime_checks,
747 };
748 assura_ast::walk_decls(&mut codegen_visitor, &source.decls);
749
750 if !matches!(config.backend, CodegenBackend::Cranelift) {
753 for decl in &source.decls {
754 if let Decl::Contract(c) = &decl.node {
755 generate_proptest_for_contract(c, &mut all_code);
756 }
757 }
758 }
759
760 let formatted_body = format_rust(&all_code);
761 let formatted = format!("{GENERATED_HEADER}{formatted_body}");
762
763 GeneratedProject {
764 cargo_toml,
765 files: vec![("src/lib.rs".to_string(), formatted)],
766 metadata: None,
767 }
768 };
769
770 let source_name = project_name.clone() + ".assura";
772 project.metadata = Some(metadata::extract_metadata(typed, &source_name));
773
774 if matches!(config.backend, CodegenBackend::Cranelift) {
776 project.files.push((
777 ".cargo/config.toml".to_string(),
778 "[unstable]\ncodegen-backend = true\n\n[profile.dev]\ncodegen-backend = \"cranelift\"\n"
779 .to_string(),
780 ));
781
782 for (path, content) in &mut project.files {
786 if path.ends_with(".rs") {
787 *content = cranelift_transform(content);
788 }
789 }
790 }
791
792 project
793}
794
795fn cranelift_transform(code: &str) -> String {
804 let mut out = String::with_capacity(code.len() + 512);
805 for line in code.lines() {
806 let trimmed = line.trim_start();
807 if (trimmed.starts_with("pub struct ") || trimmed.starts_with("pub enum "))
808 && !trimmed.contains("pub struct PhantomData")
809 {
810 let indent = &line[..line.len() - trimmed.len()];
812 out.push_str(indent);
813 out.push_str("#[repr(C)]\n");
814 out.push_str(line);
815 out.push('\n');
816 } else if trimmed.starts_with("pub fn ") && !trimmed.contains("fmt(") {
817 let indent = &line[..line.len() - trimmed.len()];
819 out.push_str(indent);
820 out.push_str("#[no_mangle]\n");
821 let transformed = line.replacen("pub fn ", "pub extern \"C\" fn ", 1);
822 out.push_str(&transformed);
823 out.push('\n');
824 } else {
825 out.push_str(line);
826 out.push('\n');
827 }
828 }
829 out
830}
831
832#[cfg(test)]
833mod cranelift_tests {
834 use super::cranelift_transform;
835
836 #[test]
837 fn transforms_pub_struct() {
838 let input = "pub struct Foo {\n x: i64,\n}\n";
839 let out = cranelift_transform(input);
840 assert!(out.contains("#[repr(C)]\npub struct Foo"));
841 }
842
843 #[test]
844 fn transforms_pub_enum() {
845 let input = "pub enum Color {\n Red,\n Blue,\n}\n";
846 let out = cranelift_transform(input);
847 assert!(out.contains("#[repr(C)]\npub enum Color"));
848 }
849
850 #[test]
851 fn transforms_pub_fn() {
852 let input = "pub fn add(a: i64, b: i64) -> i64 {\n a + b\n}\n";
853 let out = cranelift_transform(input);
854 assert!(out.contains("#[no_mangle]"));
855 assert!(out.contains("pub extern \"C\" fn add"));
856 }
857
858 #[test]
859 fn skips_private_fn() {
860 let input = "fn helper(x: i64) -> i64 { x }\n";
861 let out = cranelift_transform(input);
862 assert!(!out.contains("#[no_mangle]"));
863 assert!(!out.contains("extern \"C\""));
864 }
865
866 #[test]
867 fn skips_fmt_method() {
868 let input =
869 " pub fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n";
870 let out = cranelift_transform(input);
871 assert!(!out.contains("#[no_mangle]"));
872 assert!(!out.contains("extern \"C\""));
873 }
874
875 #[test]
876 fn empty_input() {
877 let out = cranelift_transform("");
878 assert!(!out.contains("#[repr(C)]"));
880 assert!(!out.contains("#[no_mangle]"));
881 }
882
883 #[test]
884 fn no_pub_items() {
885 let input = "fn private() {}\nstruct Hidden;\n";
886 let out = cranelift_transform(input);
887 assert!(!out.contains("#[repr(C)]"));
888 assert!(!out.contains("#[no_mangle]"));
889 }
890
891 #[test]
892 fn preserves_indentation() {
893 let input = " pub struct Inner {\n val: i32,\n }\n";
894 let out = cranelift_transform(input);
895 assert!(out.contains(" #[repr(C)]\n pub struct Inner"));
896 }
897}
898
899pub fn codegen(typed: &TypedFile) -> GeneratedProject {
904 codegen_with_config(typed, &BackendConfig::default())
905}