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