Skip to main content

assura_codegen/
lib.rs

1//! Rust code generation from type-checked Assura contracts.
2//!
3//! Takes a `TypedFile` from `assura-types` and generates a Rust project
4//! consisting of a `Cargo.toml` and one or more `.rs` source files.
5//! Generated Rust is formatted via `prettyplease`.
6//!
7//! This is T019: the initial scaffolding. Type mapping (T020), contract
8//! codegen (T021), project generation (T022), and struct/enum codegen
9//! (T023) extend this foundation.
10
11use std::fmt::Write;
12
13/// Typed Rust HIR for structured code generation.
14pub mod hir;
15/// Assura-to-Rust type mapping (Int -> i64, Nat -> u64, etc.).
16pub mod type_map;
17
18mod block;
19mod contract;
20mod decl;
21mod expr;
22/// Feature-specific code generation for all 50 verification features.
23pub mod features;
24/// Structured contract metadata for AI agent consumption.
25pub 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
47/// Lint suppression header prepended to every generated `.rs` file.
48const GENERATED_ALLOW_ATTRS: &str = "#![allow(dead_code, unused_variables, unused_imports, unused_parens, non_camel_case_types, unreachable_code)]\n\n";
49
50/// Comment header prepended to every generated `.rs` file.
51const GENERATED_HEADER: &str = "// Generated by the Assura compiler.\n// Do not edit manually.\n\n";
52
53#[cfg(test)]
54mod codegen_tests;
55
56// ---------------------------------------------------------------------------
57// Public output types
58// ---------------------------------------------------------------------------
59
60/// The result of code generation: a complete Rust project.
61#[derive(Debug, Clone)]
62pub struct GeneratedProject {
63    /// Content of the generated `Cargo.toml`.
64    pub cargo_toml: String,
65    /// Generated source files as `(relative_path, rust_source)` pairs.
66    /// Typically `[("src/lib.rs", "...")]`.
67    pub files: Vec<(String, String)>,
68    /// Structured contract metadata for AI agent consumption.
69    /// Serializable to `assura-contracts.json`.
70    pub metadata: Option<metadata::ProjectMetadata>,
71}
72
73// ---------------------------------------------------------------------------
74// T119: Backend selection (Rustc vs Cranelift for fast dev builds)
75// ---------------------------------------------------------------------------
76
77/// Code generation backend.
78#[derive(Debug, Clone, PartialEq, Default)]
79pub enum CodegenBackend {
80    /// Standard rustc backend (optimized, production).
81    #[default]
82    Rustc,
83    /// Cranelift backend (fast compilation, dev builds).
84    Cranelift,
85}
86
87/// Compilation target for the generated Rust project.
88#[derive(Debug, Clone, PartialEq, Default)]
89pub enum CompileTarget {
90    /// Native host target (default).
91    #[default]
92    Native,
93    /// WebAssembly via wasm32-wasip1 (formerly wasm32-wasi).
94    Wasm,
95}
96
97impl CompileTarget {
98    /// Parse a target string from CLI/config (e.g. "native", "wasm32-wasi").
99    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    /// The Rust target triple for cargo commands.
108    pub fn rust_target(&self) -> Option<&'static str> {
109        match self {
110            Self::Native => None, // use host default
111            Self::Wasm => Some("wasm32-wasip1"),
112        }
113    }
114}
115
116/// Configuration for code generation.
117#[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    /// Optional IR-generated Rust function bodies, keyed by contract/function name.
124    ///
125    /// When present, codegen replaces `todo!()` placeholders with real
126    /// implementations from IR sidecar files.
127    pub ir_bodies: std::collections::HashMap<String, String>,
128    /// When true, contract assertions use `assura_runtime::contract_violation`
129    /// instead of `debug_assert!`, persisting checks in release builds.
130    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    /// Create a default config with IR bodies from loaded sidecars.
148    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
156// ---------------------------------------------------------------------------
157// Phase helpers (extracted from codegen_with_config)
158// ---------------------------------------------------------------------------
159
160/// Visitor that collects user-defined type names, feature_max constants,
161/// and referenced types from all declarations.
162struct 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            // Extract type from inline value tokens (e.g., ["Nat", "=", "280"] -> "Nat")
189            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    // Prophecy / CodecRegistry: default no-op
243}
244
245/// Phase 1+2: Walk decls to collect defined type names, feature_max constants,
246/// and referenced types.
247///
248/// Returns `(defined_types, feature_max_consts, referenced_types)`.
249fn 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    // Add built-in type names that should never generate stubs
268    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
280/// Visitor that walks all type expressions in FnDef/Extern/Bind params and
281/// return types, and optionally TypeDef struct fields, applying a callback
282/// to each `TypeExpr`. Used for generic arity detection and feature_max-as-type
283/// detection without token roundtrip.
284struct 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    // Contract, Service, EnumDef, Prophecy, CodecRegistry, Block: default no-op
319}
320
321/// Phase 3: Detect feature_max names used as type arguments, working directly
322/// on `TypeExpr` instead of token vectors.
323fn 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
341/// Phase 4: Detect generic arity for type references and collect const-generic
342/// names, working directly on `TypeExpr`.
343///
344/// Returns `(type_generic_params, const_generic_names)`.
345fn 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
369/// Phase 4b: Collect SCREAMING_SNAKE_CASE names used as type arguments,
370/// combining const_generic_names from Phase 4 with feature_max names in type positions.
371fn 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
400/// Phase 5: Generate stub structs for undefined types.
401fn 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            // Skip things that aren't type names (operators, keywords, etc.)
415            !t.is_empty()
416                && t.chars().next().is_some_and(|c| c.is_uppercase())
417                && t.chars().all(|c| c.is_alphanumeric() || c == '_')
418                // Skip feature_max constants (already generated as const + type stub)
419                && !feature_max_consts.iter().any(|(n, _)| n == *t)
420                // Skip SCREAMING_SNAKE_CASE names already handled as const-as-type stubs
421                && !const_as_types.iter().any(|s| s == *t)
422                // Also skip feature_max names that are in const_as_types
423                && !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
456/// Visitor that collects contract and service names for module structure decisions.
457struct 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
471/// Visitor that generates Rust code for each declaration.
472///
473/// In multi-file mode (`include_contracts_services = false`), contracts and
474/// services are skipped (they get their own files). In single-file mode
475/// (`include_contracts_services = true`), they are generated inline.
476struct 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    // Prophecy: default no-op (ghost, erased in codegen)
530}
531
532/// Visitor that emits `pub mod` declarations for contracts and services
533/// in multi-file mode.
534struct 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    // All others: default no-op
552}
553
554// ---------------------------------------------------------------------------
555// Entry point
556// ---------------------------------------------------------------------------
557
558/// Generate a Rust project from a type-checked Assura file.
559///
560/// Walks the AST, maps Assura declarations to Rust source code, and
561/// formats the result with `prettyplease`. Returns a `GeneratedProject`
562/// with a `Cargo.toml` and generated `.rs` files.
563pub 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    // Phase 1+2: Collect defined types, feature_max consts, and referenced types.
590    let (defined_types, feature_max_consts, referenced_types) = collect_type_names(&source.decls);
591
592    // Phase 3: Detect feature_max names used as type arguments (direct TypeExpr walk).
593    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            // Will be emitted as a struct stub in Phase 4b.
598            // Also emit a const with _VALUE suffix for any value-position uses.
599            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    // Phase 4: Detect generic arity for type references and collect const-generic names.
611    let (type_generic_params, const_generic_names) = detect_generic_arities_direct(&source.decls);
612
613    // Phase 4b: Generate marker type stubs for SCREAMING_SNAKE_CASE names
614    // used as generic arguments.
615    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        // Emit as a marker type (unit struct) rather than a const,
623        // since the generic parameter positions use type params.
624        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    // Phase 5: Generate stub structs for undefined types.
634    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    // Count contracts and services to decide on module structure.
644    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        // ------------------------------------------------------------------
656        // Multi-file mode: separate .rs files for each contract/service,
657        // shared types/functions/externs in lib.rs.
658        // ------------------------------------------------------------------
659        let mut files: Vec<(String, String)> = Vec::new();
660
661        // Build shared code: types, enums, externs, functions, blocks
662        let mut shared = String::new();
663        shared.push_str(GENERATED_ALLOW_ATTRS);
664        // Emit the pre-built preamble (feature_max, const-as-type stubs,
665        // placeholder structs)
666        shared.push_str(&code);
667
668        // Generate shared code (types, enums, externs, functions, blocks).
669        // Contracts and services go into their own files in multi-file mode.
670        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        // Add pub mod declarations for each contract/service module
684        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        // Generate per-contract files
692        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 the contract body without wrapping it in
699                // `pub mod contract_xxx { ... }` since it IS the module file.
700                generate_contract_contents_opts(c, &mut mod_code, ir_ref, config.runtime_checks);
701                // S009: proptest for multi-file contracts
702                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        // Generate per-service files
710        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        // ------------------------------------------------------------------
730        // Single-file mode: everything in lib.rs (current behavior).
731        // ------------------------------------------------------------------
732        let mut all_code = String::new();
733        all_code.push_str(GENERATED_ALLOW_ATTRS);
734        all_code.push_str(&code);
735
736        // Generate code for all declarations (single-file: contracts and services inline).
737        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        // S009: Generate proptest tests for testable contracts
751        // Skip proptest for Cranelift (dev builds prioritize fast compilation)
752        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    // Generate structured contract metadata
771    let source_name = project_name.clone() + ".assura";
772    project.metadata = Some(metadata::extract_metadata(typed, &source_name));
773
774    // Add .cargo/config.toml for Cranelift backend
775    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        // Transform generated Rust code for C ABI compatibility:
783        // - Add #[repr(C)] to structs/enums (deterministic layout for JIT)
784        // - Add #[no_mangle] extern "C" to public functions (callable via JIT)
785        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
795/// Transform generated Rust code for Cranelift JIT compatibility.
796///
797/// Cranelift is used for fast dev-cycle builds. To make generated functions
798/// callable from JIT-compiled code, we:
799/// - Add `#[repr(C)]` before `pub struct` and `pub enum` (C ABI layout)
800/// - Replace `pub fn` with `#[no_mangle] pub extern "C" fn` (C ABI calling convention)
801///
802/// This does NOT affect `fn` (private functions) or impl blocks.
803fn 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            // Insert #[repr(C)] with matching indentation
811            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            // Add #[no_mangle] and extern "C" for top-level pub fn
818            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        // Empty string produces a single trailing newline from the lines iterator
879        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
899/// Generate a Rust project from a type-checked Assura file.
900///
901/// Uses default backend configuration (`Rustc`, opt-level 2, no debug info).
902/// For custom configuration, use [`codegen_with_config`].
903pub fn codegen(typed: &TypedFile) -> GeneratedProject {
904    codegen_with_config(typed, &BackendConfig::default())
905}